main
py 2,536 lines 88.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import asyncio
5 import contextlib
6 import errno
7 import socket
8 import threading
9 import time
10 from types import SimpleNamespace
11 from unittest.mock import AsyncMock, MagicMock, patch
12
13 import httpx
14 import pytest
15 import torch
16 import zmq.asyncio
17
18 from tests.v1.attention.utils import dense_kv_cache_views
19 from vllm import envs
20 from vllm.config import KVTransferConfig, set_current_vllm_config
21 from vllm.distributed.kv_transfer.kv_connector.v1.mooncake import (
22 mooncake_connector,
23 mooncake_utils,
24 )
25 from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector import (
26 _SHARED_REGION_GROUP_ID,
27 KVConnectorRole,
28 MooncakeConnector,
29 MooncakeConnectorMetadata,
30 MooncakeConnectorWorker,
31 MooncakeXferMetadata,
32 MooncakeXferResponse,
33 MooncakeXferResponseStatus,
34 PullReqMeta,
35 SendBlockMeta,
36 TransferRegion,
37 _align_transfer_regions,
38 _block_ids_for_region,
39 _coalesce_contiguous_transfer_regions,
40 _compute_sender_transfer_plan,
41 _has_opaque_packed_row,
42 _pp_mismatch_hides_packed_layers,
43 _validate_asymmetric_region_lengths,
44 get_mooncake_bootstrap_addr,
45 should_launch_bootstrap_server,
46 )
47 from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_utils import (
48 MooncakeBootstrapServer,
49 RegisterWorkerPayload,
50 )
51 from vllm.v1.kv_cache_interface import (
52 FullAttentionSpec,
53 KVCacheConfig,
54 KVCacheGroupSpec,
55 KVCacheLayout,
56 MLAAttentionSpec,
57 )
58 from vllm.v1.request import RequestStatus
59
60 from .utils import create_request, create_scheduler, create_vllm_config
61
62
63 @pytest.mark.parametrize(
64 ("remote_tp_size", "expected_dst_offsets"),
65 [
66 (1, [0, None, 65536, None, 131072, None, 196608, None]),
67 (2, [0, None, 65536, None, 0, None, 65536, None]),
68 ],
69 )
70 @pytest.mark.parametrize("local_tp_rank", range(8))
71 def test_sender_plan_replicated_gqa_tp8_to_smaller_tp(
72 remote_tp_size,
73 expected_dst_offsets,
74 local_tp_rank,
75 ):
76 expected_dst_offset = expected_dst_offsets[local_tp_rank]
77 plan = _compute_sender_transfer_plan(
78 local_tp_rank=local_tp_rank,
79 local_tp_size=8,
80 remote_tp_rank=local_tp_rank // (8 // remote_tp_size),
81 remote_tp_size=remote_tp_size,
82 local_kv_block_len=65536,
83 remote_kv_block_len=262144 // remote_tp_size,
84 producer_cache_replicated=True,
85 total_num_kv_heads=4,
86 )
87 if expected_dst_offset is None:
88 assert not plan[0]
89 else:
90 assert plan == (True, 0, expected_dst_offset, 65536)
91
92
93 @pytest.mark.parametrize(
94 ("local_tp_size", "expected_src_offsets"),
95 [
96 (1, [0, 0, 65536, 65536, 131072, 131072, 196608, 196608]),
97 (2, [0, 0, 65536, 65536, 0, 0, 65536, 65536]),
98 (4, [0] * 8),
99 ],
100 )
101 @pytest.mark.parametrize("remote_tp_rank", range(8))
102 def test_sender_plan_gqa_to_replicated_tp8(
103 local_tp_size,
104 expected_src_offsets,
105 remote_tp_rank,
106 ):
107 local_head_count = 4 // local_tp_size
108 plan = _compute_sender_transfer_plan(
109 local_tp_rank=remote_tp_rank // (8 // local_tp_size),
110 local_tp_size=local_tp_size,
111 remote_tp_rank=remote_tp_rank,
112 remote_tp_size=8,
113 local_kv_block_len=local_head_count * 65536,
114 remote_kv_block_len=65536,
115 producer_cache_replicated=False,
116 total_num_kv_heads=4,
117 )
118 assert plan == (True, expected_src_offsets[remote_tp_rank], 0, 65536)
119
120
121 @pytest.mark.parametrize(
122 "local_tp,remote_tp,local_len,remote_len,valid",
123 [
124 (1, 8, 262144, 65536, True),
125 (8, 1, 65536, 262144, True),
126 (1, 8, 262144, 131072, False),
127 (8, 1, 131072, 262144, False),
128 ],
129 )
130 def test_region_length_validation_checks_replicated_gqa_heads(
131 local_tp, remote_tp, local_len, remote_len, valid
132 ):
133 """Replicated heads must have matching, whole per-head payloads."""
134 local_region = TransferRegion("layer", 0, 0, local_len, local_len)
135 remote_region = TransferRegion("layer", 0, 0, remote_len, remote_len)
136
137 assert (
138 _validate_asymmetric_region_lengths(
139 local_regions=[local_region],
140 remote_regions=[remote_region],
141 local_tp_size=local_tp,
142 remote_tp_size=remote_tp,
143 producer_cache_replicated=local_tp > 4,
144 total_num_kv_heads=4,
145 )
146 is None
147 ) == valid
148
149
150 def _make_test_kv_cache_config() -> KVCacheConfig:
151 return KVCacheConfig(
152 num_blocks=0,
153 kv_cache_tensors=[],
154 kv_cache_groups=[
155 KVCacheGroupSpec(
156 [
157 "model.layers.0.self_attn",
158 "model.layers.1.self_attn",
159 "model.layers.0.mla_attn",
160 "model.layers.1.eagle_attn",
161 ],
162 FullAttentionSpec(
163 block_size=16,
164 num_kv_heads=4,
165 head_size=64,
166 dtype=torch.float16,
167 ),
168 )
169 ],
170 )
171
172
173 def _make_packed_mla_kv_cache_config(
174 num_blocks: int, *, num_groups: int = 1
175 ) -> KVCacheConfig:
176 spec = MLAAttentionSpec(
177 block_size=16,
178 num_kv_heads=1,
179 head_size=64,
180 dtype=torch.uint8,
181 )
182 if num_groups == 1:
183 groups = [
184 KVCacheGroupSpec(
185 ["model.layers.0.mla_attn", "model.layers.1.indexer"],
186 spec,
187 )
188 ]
189 else:
190 groups = [
191 KVCacheGroupSpec(["model.layers.0.mla_attn"], spec),
192 KVCacheGroupSpec(["model.layers.1.indexer"], spec),
193 ]
194 return KVCacheConfig(
195 num_blocks=num_blocks,
196 kv_cache_tensors=[],
197 kv_cache_groups=groups,
198 )
199
200
201 class FakeMooncakeWrapper:
202 """Mock Mooncake TransferEngine for unit testing environments."""
203
204 def __init__(self, *args, **kwargs):
205 pass
206
207 def initialize(self, local_hostname, metadata_server, protocol, device_name) -> int:
208 return 0
209
210 def get_rpc_port(self) -> int:
211 return 12345
212
213 def batch_transfer_sync_write(
214 self, target_hostname, buffers, peer_buffer_addresses, lengths
215 ) -> int:
216 return 0
217
218 def batch_register_memory(self, buffer_addresses, capacities) -> int:
219 return 0
220
221
222 def test_align_transfer_regions_uses_layer_name_occurrences():
223 """Repeated layer names should align by occurrence order."""
224 local_regions = [
225 TransferRegion(
226 layer_name="model.layers.1.self_attn",
227 layer_index=1,
228 base_addr=0x1000,
229 block_len=256,
230 kv_block_len=128,
231 ),
232 TransferRegion(
233 layer_name="model.layers.1.self_attn",
234 layer_index=1,
235 base_addr=0x1100,
236 block_len=256,
237 kv_block_len=128,
238 ),
239 ]
240 remote_regions = [
241 TransferRegion(
242 layer_name="model.layers.0.self_attn",
243 layer_index=0,
244 base_addr=0xA000,
245 block_len=256,
246 kv_block_len=128,
247 ),
248 TransferRegion(
249 layer_name="model.layers.1.self_attn",
250 layer_index=1,
251 base_addr=0xB000,
252 block_len=256,
253 kv_block_len=128,
254 ),
255 TransferRegion(
256 layer_name="model.layers.1.self_attn",
257 layer_index=1,
258 base_addr=0xB100,
259 block_len=256,
260 kv_block_len=128,
261 ),
262 ]
263
264 aligned_local, aligned_remote, err = _align_transfer_regions(
265 local_regions, remote_regions
266 )
267
268 assert err is None
269 assert [r.base_addr for r in aligned_local] == [0x1000, 0x1100]
270 assert [r.base_addr for r in aligned_remote] == [0xB000, 0xB100]
271
272 # Hetero PP with no shared layers is an empty success. Same PP fails closed.
273 p_regions = [
274 _region(
275 0x1000,
276 block_len=2048,
277 kv_block_len=1024,
278 row_offset=0,
279 layer_name="model.layers.2.mla_attn",
280 layer_index=2,
281 )
282 ]
283 early_d = [
284 _region(
285 0xA000,
286 block_len=2048,
287 kv_block_len=1024,
288 row_offset=0,
289 layer_name="model.layers.0.mla_attn",
290 layer_index=0,
291 )
292 ]
293 none_p, none_d, none_err = _align_transfer_regions(
294 p_regions, early_d, allow_partial_layers=True
295 )
296 assert none_err is None
297 assert none_p == [] and none_d == []
298 _, _, mismatch_err = _align_transfer_regions(
299 p_regions, early_d, allow_partial_layers=False
300 )
301 assert mismatch_err is not None
302
303
304 @pytest.mark.asyncio
305 async def test_build_transfer_params_separates_prefill_pp_layers():
306 """Each producer PP stage should send only its registered layer shard."""
307 worker = MooncakeConnectorWorker.__new__(MooncakeConnectorWorker)
308 worker.async_zmq_ctx = MagicMock()
309 worker.is_kv_consumer = True
310 worker.is_kv_producer = True
311 worker.tp_rank = 0
312 worker.tp_size = 1
313 worker.use_mla = False
314 worker.kv_cache_config = _make_test_kv_cache_config()
315 worker._physical_blocks_per_logical_kv_block = 1
316 worker.transfer_topo = SimpleNamespace(
317 local_replicates_kv_cache=False,
318 total_num_kv_heads=4,
319 )
320
321 block_len = 256
322 remote_regions = [
323 TransferRegion(
324 layer_name=f"model.layers.{layer_index}.self_attn",
325 layer_index=layer_index,
326 base_addr=base_addr,
327 block_len=block_len,
328 kv_block_len=block_len,
329 )
330 for layer_index, base_addr in [
331 (0, 0xA000),
332 (1, 0xB000),
333 (2, 0xC000),
334 (3, 0xD000),
335 ]
336 ]
337 producer_pp_regions = {
338 0: [
339 TransferRegion(
340 layer_name="model.layers.0.self_attn",
341 layer_index=0,
342 base_addr=0x1000,
343 block_len=block_len,
344 kv_block_len=block_len,
345 ),
346 TransferRegion(
347 layer_name="model.layers.1.self_attn",
348 layer_index=1,
349 base_addr=0x2000,
350 block_len=block_len,
351 kv_block_len=block_len,
352 ),
353 ],
354 1: [
355 TransferRegion(
356 layer_name="model.layers.2.self_attn",
357 layer_index=2,
358 base_addr=0x3000,
359 block_len=block_len,
360 kv_block_len=block_len,
361 ),
362 TransferRegion(
363 layer_name="model.layers.3.self_attn",
364 layer_index=3,
365 base_addr=0x4000,
366 block_len=block_len,
367 kv_block_len=block_len,
368 ),
369 ],
370 }
371 expected_by_pp_rank = {
372 0: {
373 "layers": [0, 1],
374 "src_ptrs": [0x1000 + 10 * block_len, 0x2000 + 10 * block_len],
375 "dst_ptrs": [0xA000 + 20 * block_len, 0xB000 + 20 * block_len],
376 },
377 1: {
378 "layers": [2, 3],
379 "src_ptrs": [0x3000 + 10 * block_len, 0x4000 + 10 * block_len],
380 "dst_ptrs": [0xC000 + 20 * block_len, 0xD000 + 20 * block_len],
381 },
382 }
383
384 transfer_id = "xfer-pp-split"
385 send_meta = SendBlockMeta(
386 p_req_id="p-req-pp",
387 transfer_id=transfer_id,
388 local_block_ids=[[10, 11]],
389 ready=asyncio.Event(),
390 )
391 xfer_meta = _xfer_meta(remote_regions, {"d-req-pp": (transfer_id, [[20, 21]])})
392
393 for pp_rank, local_regions in producer_pp_regions.items():
394 aligned_local, aligned_remote, err = _align_transfer_regions(
395 local_regions, remote_regions
396 )
397
398 assert err is None
399 assert [r.layer_index for r in aligned_local] == (
400 expected_by_pp_rank[pp_rank]["layers"]
401 )
402 assert [r.layer_index for r in aligned_remote] == (
403 expected_by_pp_rank[pp_rank]["layers"]
404 )
405
406 (
407 src_ptrs,
408 dst_ptrs,
409 lengths,
410 err_reqs,
411 err_msg,
412 ) = await worker._build_transfer_params(
413 ready_reqs=[("d-req-pp", send_meta)],
414 agent_meta=xfer_meta,
415 local_regions=aligned_local,
416 remote_regions=aligned_remote,
417 )
418
419 assert err_reqs == []
420 assert err_msg is None
421 assert src_ptrs == expected_by_pp_rank[pp_rank]["src_ptrs"]
422 assert dst_ptrs == expected_by_pp_rank[pp_rank]["dst_ptrs"]
423 assert lengths == [2 * block_len, 2 * block_len]
424
425
426 @pytest.mark.asyncio
427 async def test_send_kv_to_decode_aligns_consumer_regions_by_layer_metadata(
428 monkeypatch,
429 ):
430 """Producer sends its PP layer shard to the matching consumer layer address."""
431 monkeypatch.setenv("VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT", "5")
432 vllm_config = create_vllm_config(
433 kv_connector="MooncakeConnector", kv_role="kv_producer"
434 )
435
436 with set_current_vllm_config(vllm_config), patch_worker_dependencies():
437 prefill_connector = MooncakeConnector(
438 vllm_config,
439 KVConnectorRole.WORKER,
440 _make_test_kv_cache_config(),
441 )
442 prefill_worker = prefill_connector.connector_worker
443
444 block_len = 4096
445 prefill_worker.kv_caches_base_addr = [0x1000]
446 prefill_worker.block_len_per_layer = [block_len]
447 prefill_worker.kv_block_len_per_layer = [block_len]
448 prefill_worker.registered_layer_names = ["model.layers.1.self_attn"]
449 prefill_worker.registered_layer_indices = [1]
450
451 class InlineSenderLoop:
452 async def run_in_executor(self, executor, func, *args):
453 return func(*args)
454
455 origin_sender_loop = prefill_worker.sender_loop
456 prefill_worker.sender_loop = InlineSenderLoop()
457
458 transfer_id = "xfer-layer-align"
459 send_meta = SendBlockMeta(
460 p_req_id="p-req-layer-align",
461 transfer_id=transfer_id,
462 local_block_ids=[[10]],
463 ready=asyncio.Event(),
464 )
465 prefill_worker.reqs_need_send[transfer_id] = send_meta
466 send_meta.ready.set()
467
468 xfer_meta = _xfer_meta(
469 [
470 _region(
471 0xA000,
472 block_len=block_len,
473 row_offset=-1,
474 layer_name="model.layers.0.self_attn",
475 layer_index=0,
476 ),
477 _region(
478 0xB000,
479 block_len=block_len,
480 row_offset=-1,
481 layer_name="model.layers.1.self_attn",
482 layer_index=1,
483 ),
484 ],
485 {"d-req-layer-align": (transfer_id, [[20]])},
486 )
487 mock_socket = AsyncMock(spec=zmq.asyncio.Socket)
488 mock_socket.send_multipart = AsyncMock()
489 identity = b"consumer-layer-align"
490
491 with patch.object(
492 prefill_worker, "_send_blocks", return_value=0
493 ) as mock_send_blocks:
494 await prefill_worker.send_kv_to_decode(identity, mock_socket, xfer_meta)
495
496 src_ptrs, dst_ptrs, lengths = mock_send_blocks.call_args[0][1:]
497 assert src_ptrs == [0x1000 + 10 * block_len]
498 assert dst_ptrs == [0xB000 + 20 * block_len]
499 assert lengths == [block_len]
500
501 sent_identity, sent_payload = mock_socket.send_multipart.call_args[0][0]
502 assert sent_identity == identity
503 response = prefill_worker._xfer_resp_decoder.decode(sent_payload)
504 assert response.status == MooncakeXferResponseStatus.FINISH
505 assert response.ok_reqs == ["d-req-layer-align"]
506
507 prefill_worker.sender_loop = origin_sender_loop
508 prefill_worker.shutdown()
509
510
511 def test_basic_interface():
512 """Unit test for basic MooncakeConnector interface functionality."""
513 vllm_config = create_vllm_config(
514 kv_connector="MooncakeConnector", kv_role="kv_consumer"
515 )
516 scheduler = create_scheduler(vllm_config)
517
518 # 2 Full Blocks and 1 Half Block.
519 BLOCK_SIZE = vllm_config.cache_config.block_size
520 NUM_EXTERNAL_FULL_BLOCKS = 2
521 NUM_TOKENS = int(BLOCK_SIZE * (NUM_EXTERNAL_FULL_BLOCKS + 0.5))
522
523 request = create_request(
524 request_id=1,
525 block_size=BLOCK_SIZE,
526 num_tokens=NUM_TOKENS,
527 do_remote_prefill=True,
528 )
529 request_id = request.request_id
530 request.kv_transfer_params.update(
531 {
532 "transfer_id": request_id,
533 "remote_bootstrap_addr": 54321,
534 }
535 )
536
537 scheduler.add_request(request)
538
539 # Remote Prefill, triggers NixlConnectorMetadata.
540 scheduler_output = scheduler.schedule()
541 kv_connector_metadata = scheduler_output.kv_connector_metadata
542 assert kv_connector_metadata is not None
543 assert isinstance(kv_connector_metadata, MooncakeConnectorMetadata)
544
545 assert len(kv_connector_metadata.reqs_to_recv) == 1
546 assert request_id in kv_connector_metadata.reqs_to_recv["my-engine-id"]
547 req_meta = kv_connector_metadata.reqs_to_recv["my-engine-id"][request_id]
548
549 # local_block_ids is list[list[int]] (per-group); flatten for comparison.
550 all_block_ids = [bid for group in req_meta.local_block_ids for bid in group]
551 for block_id, block in zip(
552 all_block_ids,
553 scheduler.kv_cache_manager.coordinator.single_type_managers[0].req_to_blocks[
554 request_id
555 ],
556 ):
557 assert block_id == block.block_id
558
559
560 def test_prompt_less_than_block_size():
561 """Test that we can handle case where prompt is < block."""
562 vllm_config = create_vllm_config(
563 kv_connector="MooncakeConnector", kv_role="kv_consumer"
564 )
565 scheduler = create_scheduler(vllm_config)
566
567 # Half of a block.
568 BLOCK_SIZE = vllm_config.cache_config.block_size
569 NUM_TOKENS = int(BLOCK_SIZE * 0.5)
570
571 # Request will have 1 partial remote block.
572 request = create_request(
573 request_id=1,
574 block_size=BLOCK_SIZE,
575 num_tokens=NUM_TOKENS,
576 do_remote_prefill=True,
577 num_remote_blocks=1,
578 )
579 request.kv_transfer_params.update(
580 {
581 "transfer_id": request.request_id,
582 "remote_bootstrap_addr": 54321,
583 }
584 )
585
586 scheduler.add_request(request)
587 scheduler_output = scheduler.schedule()
588
589 # This request will read async.
590 kv_connector_metadata = scheduler_output.kv_connector_metadata
591 assert kv_connector_metadata is not None
592 assert isinstance(kv_connector_metadata, MooncakeConnectorMetadata)
593 assert len(kv_connector_metadata.reqs_to_recv["my-engine-id"]) == 1
594 assert len(scheduler_output.scheduled_new_reqs) == 0
595
596
597 @pytest.fixture
598 def bootstrap_server():
599 """Fixture to launch and cleanup a Mooncake Bootstrap HTTP Server."""
600 server = MooncakeBootstrapServer("127.0.0.1", 0)
601 try:
602 server.start()
603 yield server
604 finally:
605 server.shutdown()
606
607
608 def test_bootstrap_server_occupied_port_raises_to_caller():
609 with socket.create_server(("127.0.0.1", 0)) as listener:
610 server = MooncakeBootstrapServer("127.0.0.1", listener.getsockname()[1])
611 try:
612 with pytest.raises(OSError) as exc_info:
613 server.start(timeout=0.05)
614 assert exc_info.value.errno == errno.EADDRINUSE
615 finally:
616 server.shutdown()
617
618
619 def test_bootstrap_server_retains_and_releases_assigned_port(bootstrap_server):
620 port = bootstrap_server.port
621 assert port > 0
622 with (
623 pytest.raises(OSError) as exc_info,
624 socket.create_server(("127.0.0.1", port)),
625 ):
626 pass
627 assert exc_info.value.errno == errno.EADDRINUSE
628
629 bootstrap_server.shutdown()
630 with socket.create_server(("127.0.0.1", port)):
631 pass
632
633
634 @pytest.mark.parametrize("failure", ["exit", "error", "timeout"])
635 def test_bootstrap_server_startup_failure_releases_port(monkeypatch, failure):
636 stop = threading.Event()
637
638 def run_without_starting(server, *, sockets):
639 if failure == "error":
640 raise SystemExit(1)
641 if failure == "timeout":
642 while not server.should_exit and not stop.wait(0.01):
643 pass
644
645 monkeypatch.setattr(mooncake_utils.uvicorn.Server, "run", run_without_starting)
646 server = MooncakeBootstrapServer("127.0.0.1", 0)
647 try:
648 error = TimeoutError if failure == "timeout" else RuntimeError
649 with pytest.raises(error):
650 server.start(timeout=0.05 if failure == "timeout" else 1.0)
651 assert server.port > 0
652 with socket.create_server(("127.0.0.1", server.port)):
653 pass
654 finally:
655 stop.set()
656 server.shutdown()
657
658
659 @pytest.mark.parametrize("state", ["stopping", "exited"])
660 def test_bootstrap_server_rejects_stale_started_state(monkeypatch, state):
661 release = threading.Event()
662
663 def run_until_released(server, *, sockets):
664 server.started = True
665 release.wait()
666
667 monkeypatch.setattr(mooncake_utils.uvicorn.Server, "run", run_until_released)
668 server = MooncakeBootstrapServer("127.0.0.1", 0)
669 try:
670 server.start()
671 thread = server.server_thread
672 assert thread is not None
673 if state == "stopping":
674 with patch.object(thread, "join", return_value=None):
675 server.shutdown()
676 else:
677 release.set()
678 thread.join(timeout=5)
679 assert not thread.is_alive()
680
681 with pytest.raises(RuntimeError, match="still starting or stopping"):
682 server.start()
683 finally:
684 release.set()
685 server.shutdown()
686
687
688 @pytest.mark.asyncio
689 @pytest.mark.parametrize("external", [False, True])
690 async def test_register_worker_recovers_from_slow_bootstrap_server(
691 monkeypatch, external
692 ):
693 """End-to-end: a real HTTP server that responds slower than the configured
694 timeout must be retried on the wire, not treated as fatal."""
695 original = MooncakeBootstrapServer.register_worker
696 state = {"done": False, "calls": 0}
697
698 async def slow_register(self, payload: RegisterWorkerPayload):
699 state["calls"] += 1
700 if not state["done"]:
701 state["done"] = True
702 # Record the worker first, then stall past the client timeout, so
703 # the retry exercises the duplicate-registration path.
704 response = await original(self, payload)
705 await asyncio.sleep(1.5)
706 return response
707 return await original(self, payload)
708
709 monkeypatch.setattr(MooncakeBootstrapServer, "register_worker", slow_register)
710
711 monkeypatch.setenv("VLLM_MOONCAKE_CONNECTOR_TIMEOUT", "0.5")
712 monkeypatch.setattr(mooncake_connector, "_BOOTSTRAP_MAX_ATTEMPTS", 5)
713
714 server = MooncakeBootstrapServer("127.0.0.1", 0)
715 server.start()
716 try:
717 monkeypatch.setenv("VLLM_MOONCAKE_BOOTSTRAP_PORT", str(server.port))
718 worker = _make_local_register_worker_stub()
719 if external:
720 worker.vllm_config = _make_bootstrap_vllm_config(
721 bootstrap_server_address=f"127.0.0.1:{server.port}"
722 )
723 assert not should_launch_bootstrap_server(worker.vllm_config)
724 await MooncakeConnectorWorker.register_worker_with_bootstrap(worker)
725 assert state["calls"] == 2
726
727 async with httpx.AsyncClient() as client:
728 response = await client.get(f"http://127.0.0.1:{server.port}/query")
729 assert response.status_code == 200
730 assert response.json()["0"]["engine_id"] == "eng-1"
731 finally:
732 server.shutdown()
733
734
735 @pytest.mark.asyncio
736 async def test_bootstrap_server(bootstrap_server: MooncakeBootstrapServer):
737 """Tests the bootstrap server's api for worker registration and querying.
738
739 Validates DP/TP/PP rank indexing and error handling for duplicate registrations.
740 """
741 import httpx
742
743 base_url = f"http://127.0.0.1:{bootstrap_server.port}"
744
745 # Query when empty
746 async with httpx.AsyncClient() as client:
747 response = await client.get(f"{base_url}/query")
748 assert response.status_code == 200
749 assert response.json() == {}
750
751 # Register multiple PP workers from the same producer engine.
752 payload1 = {
753 "engine_id": "eng-1",
754 "dp_rank": 0,
755 "tp_rank": 0,
756 "pp_rank": 0,
757 "addr": "tcp://1.1.1.1:1111",
758 }
759 async with httpx.AsyncClient() as client:
760 response = await client.post(f"{base_url}/register", json=payload1)
761 assert response.status_code == 200
762 assert response.json() == {"status": "ok"}
763
764 payload2 = {
765 "engine_id": "eng-1",
766 "dp_rank": 0,
767 "tp_rank": 0,
768 "pp_rank": 1,
769 "addr": "tcp://2.2.2.2:2222",
770 }
771 async with httpx.AsyncClient() as client:
772 response = await client.post(f"{base_url}/register", json=payload2)
773 assert response.status_code == 200
774 assert response.json() == {"status": "ok"}
775
776 # Query after registration should preserve the PP dimension.
777 async with httpx.AsyncClient() as client:
778 response = await client.get(f"{base_url}/query")
779 assert response.status_code == 200
780 data = response.json()
781 assert "0" in data
782 assert data["0"]["engine_id"] == "eng-1"
783 assert data["0"]["worker_addr"]["0"]["0"] == "tcp://1.1.1.1:1111"
784 assert data["0"]["worker_addr"]["0"]["1"] == "tcp://2.2.2.2:2222"
785
786 # Re-registering the identical payload is idempotent.
787 async with httpx.AsyncClient() as client:
788 response = await client.post(f"{base_url}/register", json=payload1)
789 assert response.status_code == 200
790 assert response.json() == {"status": "ok"}
791
792 # Test failure: same ranks, conflicting address
793 payload1_conflict = dict(payload1, addr="tcp://9.9.9.9:9999")
794 async with httpx.AsyncClient() as client:
795 response = await client.post(f"{base_url}/register", json=payload1_conflict)
796 assert response.status_code == 400
797 assert "is already registered" in response.text
798
799 # Test failure: engine_id mismatch for same dp_rank
800 payload3_fail = {
801 "engine_id": "eng-2",
802 "dp_rank": 0,
803 "tp_rank": 1,
804 "pp_rank": 0,
805 "addr": "tcp://3.3.3.3:3333",
806 }
807 async with httpx.AsyncClient() as client:
808 response = await client.post(f"{base_url}/register", json=payload3_fail)
809 assert response.status_code == 400
810 assert "Engine ID mismatch" in response.text
811
812
813 def _make_bootstrap_vllm_config(
814 *,
815 local_engines_only: bool = False,
816 data_parallel_rank_local: int = 0,
817 data_parallel_index: int = 0,
818 nnodes_within_dp: int = 1,
819 bootstrap_server_address: str | None = None,
820 ) -> SimpleNamespace:
821 return SimpleNamespace(
822 kv_transfer_config=KVTransferConfig(
823 kv_connector="MooncakeConnector",
824 kv_role="kv_producer",
825 kv_connector_extra_config={
826 "bootstrap_server_address": bootstrap_server_address
827 },
828 ),
829 parallel_config=SimpleNamespace(
830 local_engines_only=local_engines_only,
831 data_parallel_rank_local=data_parallel_rank_local,
832 data_parallel_index=data_parallel_index,
833 nnodes_within_dp=nnodes_within_dp,
834 master_addr="model-parallel-master",
835 data_parallel_master_ip="data-parallel-master",
836 ),
837 )
838
839
840 def _make_local_register_worker_stub():
841 return SimpleNamespace(
842 vllm_config=SimpleNamespace(
843 kv_transfer_config=None,
844 parallel_config=SimpleNamespace(
845 local_engines_only=False,
846 data_parallel_rank_local=0,
847 data_parallel_index=0,
848 nnodes_within_dp=1,
849 master_addr="127.0.0.1",
850 data_parallel_master_ip="127.0.0.1",
851 ),
852 ),
853 hostname="127.0.0.1",
854 side_channel_port=1234,
855 engine_id="eng-1",
856 dp_rank=0,
857 tp_rank=0,
858 pp_rank=0,
859 )
860
861
862 def _make_register_worker_stub():
863 return SimpleNamespace(
864 vllm_config=_make_bootstrap_vllm_config(),
865 hostname="127.0.0.1",
866 side_channel_port=1234,
867 engine_id="eng-1",
868 dp_rank=0,
869 tp_rank=0,
870 pp_rank=0,
871 )
872
873
874 class _FlakyAsyncClient:
875 """httpx.AsyncClient stub that read-times-out `failures` times first."""
876
877 def __init__(self, failures: int, calls: list[int], **kwargs):
878 self.failures = failures
879 self.calls = calls
880
881 async def __aenter__(self):
882 return self
883
884 async def __aexit__(self, *exc_info):
885 return False
886
887 async def post(self, url, json=None):
888 self.calls.append(1)
889 if len(self.calls) <= self.failures:
890 raise httpx.ReadTimeout("simulated slow bootstrap server")
891 response = MagicMock()
892 response.raise_for_status = MagicMock()
893 return response
894
895
896 @pytest.mark.asyncio
897 async def test_register_worker_retries_on_read_timeout(monkeypatch):
898 """A slow rank-0 bootstrap response must be retried, not treated as fatal."""
899 monkeypatch.setattr(mooncake_connector, "_BOOTSTRAP_MAX_ATTEMPTS", 5)
900 monkeypatch.setattr(asyncio, "sleep", AsyncMock())
901
902 calls: list[int] = []
903 monkeypatch.setattr(
904 httpx,
905 "AsyncClient",
906 lambda **kwargs: _FlakyAsyncClient(2, calls),
907 )
908
909 worker = _make_register_worker_stub()
910 await MooncakeConnectorWorker.register_worker_with_bootstrap(worker)
911
912 assert len(calls) == 3
913
914
915 @pytest.mark.asyncio
916 async def test_register_worker_raises_after_max_attempts(monkeypatch):
917 """Terminal registration failure must raise, not loop forever."""
918 monkeypatch.setattr(mooncake_connector, "_BOOTSTRAP_MAX_ATTEMPTS", 3)
919 monkeypatch.setattr(asyncio, "sleep", AsyncMock())
920
921 calls: list[int] = []
922 monkeypatch.setattr(
923 httpx,
924 "AsyncClient",
925 lambda **kwargs: _FlakyAsyncClient(99, calls),
926 )
927
928 worker = _make_register_worker_stub()
929 with pytest.raises(RuntimeError, match="after 3 attempts"):
930 await MooncakeConnectorWorker.register_worker_with_bootstrap(worker)
931
932 assert len(calls) == 3
933
934
935 @pytest.mark.asyncio
936 async def test_sender_listener_failure_propagates_to_caller(monkeypatch):
937 """A terminal registration failure must surface via the Future rather than
938 leaving register_kv_caches blocked on its ready event."""
939 monkeypatch.setenv("VLLM_MOONCAKE_CONNECTOR_TIMEOUT", "0.1")
940 monkeypatch.setattr(mooncake_connector, "_BOOTSTRAP_MAX_ATTEMPTS", 1)
941
942 loop = asyncio.new_event_loop()
943 thread = threading.Thread(target=loop.run_forever, daemon=True)
944 thread.start()
945
946 async def failing_listener(ready_event):
947 raise RuntimeError("simulated terminal registration failure")
948
949 try:
950 ready_event = threading.Event()
951 fut = asyncio.run_coroutine_threadsafe(failing_listener(ready_event), loop)
952
953 deadline = time.monotonic() + 10.0
954 raised = None
955 while not ready_event.wait(timeout=0.1):
956 if fut.done():
957 try:
958 fut.result()
959 except RuntimeError as e:
960 raised = e
961 break
962 assert time.monotonic() < deadline, "caller blocked past its deadline"
963
964 assert raised is not None
965 assert "simulated terminal registration failure" in str(raised)
966 finally:
967 loop.call_soon_threadsafe(loop.stop)
968 thread.join(timeout=5)
969 loop.close()
970
971
972 @pytest.mark.parametrize(
973 (
974 "tp_rank",
975 "pp_rank",
976 "local_engines_only",
977 "data_parallel_rank_local",
978 "data_parallel_index",
979 "expected",
980 ),
981 [
982 (1, 0, False, 0, 0, False),
983 (0, 1, False, 0, 0, False),
984 (0, 0, True, 0, 1, True),
985 (0, 0, True, 1, 0, False),
986 (0, 0, False, 0, 0, True),
987 (0, 0, False, 0, 1, False),
988 ],
989 ids=[
990 "nonzero_tp_rank",
991 "nonzero_pp_rank",
992 "local_engine_rank_zero",
993 "local_engine_nonzero_rank",
994 "internal_lb_first_dp_engine",
995 "internal_lb_nonzero_dp_engine",
996 ],
997 )
998 def test_should_launch_bootstrap_server_selects_single_owner(
999 tp_rank: int,
1000 pp_rank: int,
1001 local_engines_only: bool,
1002 data_parallel_rank_local: int,
1003 data_parallel_index: int,
1004 expected: bool,
1005 ):
1006 vllm_config = _make_bootstrap_vllm_config(
1007 local_engines_only=local_engines_only,
1008 data_parallel_rank_local=data_parallel_rank_local,
1009 data_parallel_index=data_parallel_index,
1010 )
1011 with (
1012 patch(
1013 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake."
1014 "mooncake_connector.get_tensor_model_parallel_rank",
1015 return_value=tp_rank,
1016 ),
1017 patch(
1018 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake."
1019 "mooncake_connector.get_pp_group"
1020 ) as mock_pp_group,
1021 ):
1022 mock_pp_group.return_value.rank_in_group = pp_rank
1023 assert should_launch_bootstrap_server(vllm_config) is expected
1024
1025
1026 @pytest.mark.parametrize(
1027 ("local_engines_only", "nnodes_within_dp", "expected_host"),
1028 [
1029 (True, 2, "127.0.0.1"),
1030 (False, 2, "model-parallel-master"),
1031 (False, 1, "data-parallel-master"),
1032 ],
1033 ids=["local_engine", "multi_node_tp_or_pp", "single_node_internal_lb"],
1034 )
1035 def test_get_mooncake_bootstrap_addr_selects_expected_host(
1036 local_engines_only: bool,
1037 nnodes_within_dp: int,
1038 expected_host: str,
1039 ):
1040 vllm_config = _make_bootstrap_vllm_config(
1041 local_engines_only=local_engines_only,
1042 nnodes_within_dp=nnodes_within_dp,
1043 )
1044
1045 assert get_mooncake_bootstrap_addr(vllm_config) == (
1046 expected_host,
1047 envs.VLLM_MOONCAKE_BOOTSTRAP_PORT,
1048 )
1049
1050
1051 @pytest.mark.parametrize(
1052 ("address", "expected"),
1053 [("external:12345", ("external", 12345)), ("[::1]:12345", ("::1", 12345))],
1054 )
1055 def test_get_mooncake_bootstrap_addr_uses_external_endpoint(address, expected):
1056 config = _make_bootstrap_vllm_config(bootstrap_server_address=address)
1057 assert get_mooncake_bootstrap_addr(config) == expected
1058
1059
1060 @pytest.mark.parametrize("address", [":12345", "host:0", "host:65536"])
1061 def test_get_mooncake_bootstrap_addr_rejects_invalid_external_address(address):
1062 config = _make_bootstrap_vllm_config(bootstrap_server_address=address)
1063 with pytest.raises(ValueError, match="bootstrap_server_address"):
1064 get_mooncake_bootstrap_addr(config)
1065
1066
1067 def test_scheduler_request_finished():
1068 """Tests the scheduler-side logic when a request finishes.
1069
1070 Differentiates between 'Finished' (requires transfer)
1071 and 'Aborted' (immediate free).
1072 """
1073 vllm_config = create_vllm_config(
1074 kv_connector="MooncakeConnector", kv_role="kv_producer"
1075 )
1076 scheduler = create_scheduler(vllm_config)
1077 scheduler_connector = scheduler.get_kv_connector().connector_scheduler
1078
1079 request = create_request(request_id=1, do_remote_decode=True)
1080 request.kv_transfer_params["transfer_id"] = request.request_id
1081
1082 # Case: Capped length (Successful prefill, need to send to decoder)
1083 request.status = RequestStatus.FINISHED_LENGTH_CAPPED
1084 delay_free, _ = scheduler_connector.request_finished(request, block_ids=([10, 11],))
1085 assert delay_free is True
1086 assert "id-1" in scheduler_connector._reqs_need_send
1087 assert scheduler_connector._reqs_need_send["id-1"][1] == [[10, 11]]
1088
1089 # Case: Aborted (No need to transfer, free blocks immediately)
1090 scheduler_connector._reqs_need_send.clear()
1091 request.status = RequestStatus.FINISHED_ABORTED
1092 delay_free, _ = scheduler_connector.request_finished(request, block_ids=([12],))
1093 assert delay_free is False
1094 assert len(scheduler_connector._reqs_need_send) == 0
1095 assert "id-1" in scheduler_connector._reqs_not_processed
1096
1097
1098 @contextlib.contextmanager
1099 def patch_worker_dependencies():
1100 """Helper to mock all distributed and network dependencies for Worker tests."""
1101 with (
1102 patch(
1103 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.TransferEngine",
1104 FakeMooncakeWrapper,
1105 ),
1106 patch(
1107 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.get_ip",
1108 return_value="127.0.0.1",
1109 ),
1110 patch(
1111 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.get_tensor_model_parallel_rank",
1112 return_value=0,
1113 ),
1114 patch(
1115 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.get_tensor_model_parallel_world_size",
1116 return_value=1,
1117 ),
1118 patch(
1119 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.get_pp_group"
1120 ) as mock_pp,
1121 patch(
1122 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.should_launch_bootstrap_server",
1123 return_value=False,
1124 ),
1125 patch(
1126 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.make_zmq_socket"
1127 ) as mock_make_zmq,
1128 patch("httpx.AsyncClient") as mock_async_client,
1129 ):
1130 # Mock PP group
1131 mock_pp_group = MagicMock()
1132 mock_pp_group.rank_in_group = 0
1133 mock_pp.return_value = mock_pp_group
1134
1135 # Mock ZMQ socket
1136 mock_socket_object = AsyncMock()
1137 mock_socket_object.setsockopt = MagicMock()
1138 mock_socket_ctx = MagicMock()
1139 mock_socket_ctx.__enter__.return_value = mock_socket_object
1140 mock_make_zmq.return_value = mock_socket_ctx
1141
1142 # Mock httpx client
1143 mock_http_client_instance = AsyncMock()
1144 mock_async_client.return_value = mock_http_client_instance
1145
1146 yield {
1147 "mock_make_zmq": mock_make_zmq,
1148 "mock_socket_object": mock_socket_object,
1149 "mock_async_client": mock_async_client,
1150 "mock_http_client": mock_http_client_instance,
1151 }
1152
1153
1154 def _region(
1155 base: int,
1156 *,
1157 block_len: int = 256,
1158 kv_block_len: int | None = None,
1159 row_offset: int | None = None,
1160 layer_name: str = "layer",
1161 layer_index: int = 0,
1162 group_index: int = 0,
1163 shared_group_ids: tuple[int, ...] = (),
1164 ) -> TransferRegion:
1165 """One transfer slice. `row_offset` defaults to `base` for in-row addresses."""
1166 return TransferRegion(
1167 layer_name=layer_name,
1168 layer_index=layer_index,
1169 base_addr=base,
1170 block_len=block_len,
1171 kv_block_len=block_len if kv_block_len is None else kv_block_len,
1172 group_index=group_index,
1173 shared_group_ids=shared_group_ids,
1174 row_offset=base if row_offset is None else row_offset,
1175 )
1176
1177
1178 def _xfer_meta(
1179 regions: list[TransferRegion],
1180 req_blocks: dict,
1181 **overrides,
1182 ) -> MooncakeXferMetadata:
1183 """Handshake metadata derived from the regions under test."""
1184 fields = dict(
1185 remote_hostname="consumer-host",
1186 remote_port=54321,
1187 remote_tp_size=1,
1188 remote_tp_rank=0,
1189 req_blocks=req_blocks,
1190 kv_caches_base_addr=[region.base_addr for region in regions],
1191 block_lens=[region.block_len for region in regions],
1192 kv_block_lens=[region.kv_block_len for region in regions],
1193 registered_layer_names=[region.layer_name for region in regions],
1194 registered_layer_indices=[region.layer_index for region in regions],
1195 registered_group_indices=[region.group_index for region in regions],
1196 registered_shared_group_ids=[
1197 list(region.shared_group_ids) for region in regions
1198 ],
1199 registered_row_offsets=[region.row_offset for region in regions],
1200 )
1201 fields.update(overrides)
1202 return MooncakeXferMetadata(**fields)
1203
1204
1205 @contextlib.contextmanager
1206 def mooncake_register_worker(kv_cache_config, *, kv_role: str = "kv_consumer"):
1207 """Build a worker whose engine registration and sender thread are patched."""
1208 vllm_config = create_vllm_config(kv_connector="MooncakeConnector", kv_role=kv_role)
1209 connector_mod = (
1210 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector"
1211 )
1212 with (
1213 set_current_vllm_config(vllm_config),
1214 patch_worker_dependencies(),
1215 patch(f"{connector_mod}.threading.Event"),
1216 patch(f"{connector_mod}.threading.Thread") as mock_thread,
1217 ):
1218 connector = MooncakeConnector(
1219 vllm_config, KVConnectorRole.WORKER, kv_cache_config
1220 )
1221 worker = connector.connector_worker
1222 mock_thread.return_value.is_alive.return_value = False
1223 with patch.object(
1224 worker.engine, "batch_register_memory", return_value=0
1225 ) as reg:
1226 yield worker, reg
1227
1228
1229 @pytest.mark.asyncio
1230 @pytest.mark.parametrize(
1231 ("local_pp_size", "local_pp_rank", "expected_addrs"),
1232 [
1233 (1, 0, ["tcp://producer-pp0:1234", "tcp://producer-pp1:1234"]),
1234 (2, 1, ["tcp://producer-pp1:1234"]),
1235 ],
1236 ids=["heterogeneous_pp_pulls_all_remote_pp", "matching_pp_pulls_same_rank"],
1237 )
1238 async def test_receive_kv_selects_remote_pp_workers(
1239 local_pp_size: int,
1240 local_pp_rank: int,
1241 expected_addrs: list[str],
1242 ):
1243 """Decode workers should not hard-code producer pp_rank 0."""
1244 vllm_config = create_vllm_config(
1245 kv_connector="MooncakeConnector", kv_role="kv_consumer"
1246 )
1247
1248 with set_current_vllm_config(vllm_config), patch_worker_dependencies():
1249 decode_connector = MooncakeConnector(
1250 vllm_config,
1251 KVConnectorRole.WORKER,
1252 _make_test_kv_cache_config(),
1253 )
1254 decode_worker = decode_connector.connector_worker
1255 decode_worker.pp_size = local_pp_size
1256 decode_worker.pp_rank = local_pp_rank
1257 decode_worker._remote_agents = {
1258 "p-engine": {
1259 0: {
1260 0: "tcp://producer-pp0:1234",
1261 1: "tcp://producer-pp1:1234",
1262 }
1263 }
1264 }
1265 decode_worker._tp_size["p-engine"] = 1
1266
1267 pull_metas = {
1268 "d-req-1": PullReqMeta(
1269 d_req_id="d-req-1",
1270 transfer_id="xfer-req-1",
1271 local_block_ids=[[100, 101]],
1272 remote_engine_id="p-engine",
1273 remote_bootstrap_addr="http://bootstrap:33333",
1274 )
1275 }
1276 seen_addrs: list[str] = []
1277
1278 async def fake_receive(worker_addr: str, metas: dict[str, PullReqMeta]):
1279 seen_addrs.append(worker_addr)
1280 for meta in metas.values():
1281 meta.pull_tasks_count -= 1
1282
1283 with patch.object(
1284 decode_worker,
1285 "receive_kv_from_single_worker",
1286 side_effect=fake_receive,
1287 ):
1288 decode_worker.receive_kv("p-engine", pull_metas)
1289 await asyncio.sleep(0)
1290
1291 assert seen_addrs == expected_addrs
1292 assert pull_metas["d-req-1"].pull_tasks_count == 0
1293 decode_worker.shutdown()
1294
1295
1296 def test_resolve_need_send_accounts_for_remote_tp_fanout():
1297 """Producer-side completion waits for every paired consumer TP pull."""
1298 worker = MooncakeConnectorWorker.__new__(MooncakeConnectorWorker)
1299 worker.async_zmq_ctx = MagicMock()
1300 worker.is_kv_consumer = True
1301 worker.is_kv_producer = True
1302 send_meta = SendBlockMeta(
1303 p_req_id="p-req-1",
1304 transfer_id="xfer-req-1",
1305 local_block_ids=[[1]],
1306 ready=asyncio.Event(),
1307 )
1308
1309 worker.resolve_need_send(send_meta, remote_tp_ranks=[0, 1])
1310
1311 assert send_meta.need_send == 2
1312
1313
1314 @pytest.mark.asyncio
1315 async def test_heterogeneous_pp_waits_for_consumer_without_shared_layers():
1316 """P4/D2 must retain KV until both D stages finish, including a no-op pull."""
1317 config = create_vllm_config(kv_connector="MooncakeConnector", kv_role="kv_producer")
1318 with (
1319 set_current_vllm_config(config),
1320 patch_worker_dependencies(),
1321 patch.object(MooncakeConnectorWorker, "_sync_block_size_with_kernel"),
1322 ):
1323 worker = MooncakeConnector(
1324 config, KVConnectorRole.WORKER, _make_test_kv_cache_config()
1325 ).connector_worker
1326 try:
1327 worker.pp_size = 4
1328 worker.kv_caches_base_addr = [0x1000]
1329 worker.block_len_per_layer = [256]
1330 worker.kv_block_len_per_layer = [256]
1331 worker.registered_layer_names = ["model.layers.0.self_attn"]
1332 worker.registered_layer_indices = [0]
1333 send_meta = SendBlockMeta(
1334 p_req_id="p-req",
1335 transfer_id="transfer",
1336 local_block_ids=[[1]],
1337 ready=asyncio.Event(),
1338 )
1339 send_meta.ready.set()
1340 worker.reqs_need_send["transfer"] = send_meta
1341 sock = AsyncMock(spec=zmq.asyncio.Socket, send_multipart=AsyncMock())
1342 with (
1343 patch.object(worker, "sender_loop", asyncio.get_running_loop()),
1344 patch.object(worker, "_send_blocks", return_value=0) as send,
1345 ):
1346 for pp_rank in range(2):
1347 metadata = _xfer_meta(
1348 [
1349 _region(
1350 0x2000,
1351 row_offset=-1,
1352 layer_name=f"model.layers.{pp_rank}.self_attn",
1353 layer_index=pp_rank,
1354 )
1355 ],
1356 {"d-req": ("transfer", [[2]])},
1357 remote_hostname="consumer",
1358 remote_port=1234 + pp_rank,
1359 remote_pp_size=2,
1360 )
1361 await worker.send_kv_to_decode(b"consumer", sock, metadata)
1362 response = worker._xfer_resp_decoder.decode(
1363 sock.send_multipart.call_args.args[0][1]
1364 )
1365 assert response.status == MooncakeXferResponseStatus.FINISH
1366 assert response.ok_reqs == ["d-req"]
1367 assert ("transfer" in worker.reqs_need_send) == (pp_rank == 0)
1368 assert worker.finished_sending_reqs == (
1369 set() if pp_rank == 0 else {"p-req"}
1370 )
1371 send.assert_called_once_with("consumer:1234", [0x1100], [0x2200], [256])
1372 finally:
1373 worker.shutdown()
1374 worker.is_kv_consumer = True
1375
1376
1377 @pytest.mark.asyncio
1378 @patch(
1379 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.TransferEngine",
1380 FakeMooncakeWrapper,
1381 )
1382 async def test_kv_producer(monkeypatch):
1383 """Simulates a Producer Worker (Prefiller) receiving a transfer request
1384 from a Consumer (Decoder).
1385
1386 Verifies memory offset calculation: ptr = base_addr + block_id * block_len.
1387 """
1388 monkeypatch.setenv("VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT", "5")
1389 vllm_config = create_vllm_config(
1390 kv_connector="MooncakeConnector", kv_role="kv_producer"
1391 )
1392
1393 with set_current_vllm_config(vllm_config), patch_worker_dependencies():
1394 prefill_connector = MooncakeConnector(
1395 vllm_config,
1396 KVConnectorRole.WORKER,
1397 _make_test_kv_cache_config(),
1398 )
1399 prefill_worker = prefill_connector.connector_worker
1400 prefill_worker.kv_caches_base_addr = [0x1000]
1401 block_len = 4096
1402 prefill_worker.block_len_per_layer = [block_len]
1403 prefill_worker.kv_block_len_per_layer = [block_len]
1404 prefill_worker.registered_layer_names = ["model.layers.0.self_attn"]
1405 prefill_worker.registered_layer_indices = [0]
1406
1407 # Override loop to use current test loop
1408 origin_sender_loop = prefill_worker.sender_loop
1409 prefill_worker.sender_loop = asyncio.get_event_loop()
1410
1411 # A request is finished on Producer and ready to be sent.
1412 transfer_id = "xfer-req-1"
1413 send_meta = SendBlockMeta(
1414 p_req_id="p-req-1",
1415 transfer_id=transfer_id,
1416 local_block_ids=[[10, 11]],
1417 ready=asyncio.Event(),
1418 )
1419 prefill_worker.reqs_need_send[transfer_id] = send_meta
1420 send_meta.ready.set()
1421
1422 # Remote consumer request metadata
1423 xfer_meta = _xfer_meta(
1424 [
1425 _region(
1426 0x2000,
1427 block_len=block_len,
1428 row_offset=-1,
1429 layer_name="model.layers.0.self_attn",
1430 layer_index=0,
1431 )
1432 ],
1433 {"d-req-1": (transfer_id, [[20, 21]])},
1434 )
1435
1436 mock_socket = AsyncMock(spec=zmq.asyncio.Socket)
1437 mock_socket.send_multipart = AsyncMock()
1438 identity = b"consumer-id"
1439
1440 with patch.object(
1441 prefill_worker, "_send_blocks", return_value=0
1442 ) as mock_send_blocks:
1443
1444 def expected_transfers(src_base, dst_base, src_blocks, dst_blocks):
1445 n = len(src_blocks)
1446 return (
1447 [src_base + src_blocks[0] * block_len],
1448 [dst_base + dst_blocks[0] * block_len],
1449 [n * block_len],
1450 )
1451
1452 # Normal case: 2 blocks to 2 blocks
1453 await prefill_worker.send_kv_to_decode(identity, mock_socket, xfer_meta)
1454 src, dst, lens = expected_transfers(0x1000, 0x2000, [10, 11], [20, 21])
1455 mock_send_blocks.assert_called_once_with(
1456 "consumer-host:54321",
1457 src,
1458 dst,
1459 lens,
1460 )
1461 mock_socket.send_multipart.assert_called_once()
1462
1463 # Verify the response sent back to the consumer
1464 sent_call = mock_socket.send_multipart.call_args[0][0]
1465 sent_identity, sent_payload = sent_call
1466 assert sent_identity == identity
1467 response = prefill_worker._xfer_resp_decoder.decode(sent_payload)
1468 assert response.status == MooncakeXferResponseStatus.FINISH
1469 assert response.ok_reqs == ["d-req-1"]
1470
1471 # Verify internal state cleanup
1472 assert transfer_id not in prefill_worker.reqs_need_send
1473 assert "p-req-1" in prefill_worker.finished_sending_reqs
1474
1475 # More cases:
1476 # Consumer only needs 1 block (less than P)
1477 mock_send_blocks.reset_mock()
1478 mock_socket.send_multipart.reset_mock()
1479 prefill_worker.reqs_need_send[transfer_id] = send_meta
1480 send_meta.sent = 0
1481 send_meta.ready.set()
1482 xfer_meta.req_blocks["d-req-1"] = (transfer_id, [[20]])
1483 # Worker processes the consumer's request
1484 await prefill_worker.send_kv_to_decode(identity, mock_socket, xfer_meta)
1485 # Verify transfer parameters are correct: 11 to 20
1486 src, dst, lens = expected_transfers(0x1000, 0x2000, [11], [20])
1487 mock_send_blocks.assert_called_once_with(
1488 "consumer-host:54321",
1489 src,
1490 dst,
1491 lens,
1492 )
1493 mock_socket.send_multipart.assert_called_once()
1494
1495 # Consumer needs 3 blocks (more than P, error case)
1496 mock_send_blocks.reset_mock()
1497 mock_socket.send_multipart.reset_mock()
1498 prefill_worker.reqs_need_send[transfer_id] = send_meta
1499 send_meta.sent = 0
1500 send_meta.ready.set()
1501 xfer_meta.req_blocks["d-req-1"] = (transfer_id, [[20, 21, 22]])
1502 # Worker processes the consumer's request
1503 await prefill_worker.send_kv_to_decode(identity, mock_socket, xfer_meta)
1504 # This should not be called because error.
1505 mock_send_blocks.assert_not_called()
1506 mock_socket.send_multipart.assert_called_once()
1507 _, sent_payload = mock_socket.send_multipart.call_args[0][0]
1508 response = prefill_worker._xfer_resp_decoder.decode(sent_payload)
1509 assert response.err_msg == "P num blocks less than D"
1510 assert response.err_reqs == ["d-req-1"]
1511
1512 # Timeout
1513 mock_send_blocks.reset_mock()
1514 mock_socket.send_multipart.reset_mock()
1515 prefill_worker.reqs_need_send[transfer_id] = send_meta
1516 send_meta.sent = 0
1517 send_meta.ready.clear()
1518 xfer_meta.req_blocks["d-req-1"] = (transfer_id, [[20, 21]])
1519 # Worker processes the consumer's request
1520 await prefill_worker.send_kv_to_decode(identity, mock_socket, xfer_meta)
1521 # This should not be called because timeout.
1522 mock_send_blocks.assert_not_called()
1523 mock_socket.send_multipart.assert_called_once()
1524 _, sent_payload = mock_socket.send_multipart.call_args[0][0]
1525 response = prefill_worker._xfer_resp_decoder.decode(sent_payload)
1526 assert response.err_msg == "Timeout waiting for P side ready."
1527 assert response.err_reqs == ["d-req-1"]
1528
1529 # Transfer error
1530 with patch.object(
1531 prefill_worker, "_send_blocks", return_value=123
1532 ) as mock_send_blocks:
1533 mock_socket.send_multipart.reset_mock()
1534 prefill_worker.reqs_need_send[transfer_id] = send_meta
1535 send_meta.sent = 0
1536 send_meta.ready.set()
1537 xfer_meta.req_blocks["d-req-1"] = (transfer_id, [[20, 21]])
1538 # Worker processes the consumer's request
1539 await prefill_worker.send_kv_to_decode(identity, mock_socket, xfer_meta)
1540 mock_send_blocks.assert_called_once()
1541 mock_socket.send_multipart.assert_called_once()
1542 _, sent_payload = mock_socket.send_multipart.call_args[0][0]
1543 response = prefill_worker._xfer_resp_decoder.decode(sent_payload)
1544 assert response.err_msg == "Mooncake transfer engine returned 123"
1545 assert response.err_reqs == ["d-req-1"]
1546
1547 # Clean up
1548 prefill_worker.sender_loop = origin_sender_loop
1549 prefill_worker.shutdown()
1550
1551
1552 @pytest.mark.asyncio
1553 async def test_kv_consumuer(monkeypatch):
1554 """Simulates a Consumer Worker (Decoder) initiating a pull from a Producer.
1555
1556 Verifies that MooncakeXferMetadata is correctly serialized and sent via ZMQ.
1557 """
1558 vllm_config = create_vllm_config(
1559 kv_connector="MooncakeConnector", kv_role="kv_consumer"
1560 )
1561
1562 with set_current_vllm_config(vllm_config), patch_worker_dependencies() as mocks:
1563 decode_connector = MooncakeConnector(
1564 vllm_config,
1565 KVConnectorRole.WORKER,
1566 _make_test_kv_cache_config(),
1567 )
1568 decode_worker = decode_connector.connector_worker
1569 decode_worker.kv_caches_base_addr = [0x1000]
1570 decode_worker.block_len_per_layer = [4096]
1571 decode_worker.kv_block_len_per_layer = [4096]
1572 decode_worker.registered_layer_names = ["model.layers.0.self_attn"]
1573 decode_worker.registered_layer_indices = [0]
1574 decode_worker.rpc_port = 54321
1575
1576 # A request to pull data arrives.
1577 pull_metas = {
1578 "d-req-1": PullReqMeta(
1579 d_req_id="d-req-1",
1580 transfer_id="xfer-req-1",
1581 local_block_ids=[[100, 101]],
1582 remote_engine_id="p-engine",
1583 remote_bootstrap_addr="http://bootstrap:33333",
1584 pull_tasks_count=1,
1585 )
1586 }
1587 decode_worker._remote_agents = {"p-engine": {0: {0: "tcp://producer:1234"}}}
1588 decode_worker._tp_size["p-engine"] = 1
1589
1590 # Mock the response from the producer.
1591 mock_response = MooncakeXferResponse(
1592 status=MooncakeXferResponseStatus.FINISH, ok_reqs=["d-req-1"]
1593 )
1594 encoded_response = decode_worker._encoder.encode(mock_response)
1595 mocks["mock_socket_object"].recv.return_value = encoded_response
1596
1597 # Trigger the receive logic.
1598 decode_worker.receive_kv("p-engine", pull_metas)
1599 await asyncio.sleep(1) # Allow async task to run
1600
1601 # Verify the metadata sent to the producer.
1602 mocks["mock_make_zmq"].assert_called_with(
1603 decode_worker.async_zmq_ctx,
1604 "tcp://producer:1234",
1605 zmq.DEALER,
1606 bind=False,
1607 linger=0,
1608 )
1609 sent_payload = mocks["mock_socket_object"].send.call_args[0][0]
1610 sent_meta = decode_worker._xfer_meta_decoder.decode(sent_payload)
1611
1612 assert sent_meta.remote_hostname == "127.0.0.1"
1613 assert sent_meta.remote_port == 54321
1614 assert sent_meta.req_blocks["d-req-1"] == ("xfer-req-1", [[100, 101]])
1615 assert sent_meta.kv_caches_base_addr == [0x1000]
1616 assert sent_meta.block_lens == [4096]
1617 assert sent_meta.registered_layer_names == ["model.layers.0.self_attn"]
1618 assert sent_meta.registered_layer_indices == [0]
1619
1620 # Verify internal state is updated correctly.
1621 assert "d-req-1" in decode_worker.finished_recving_reqs
1622
1623 # Clean up
1624 decode_worker.shutdown()
1625
1626
1627 @pytest.mark.asyncio
1628 async def test_worker_get_finished_timeout(monkeypatch):
1629 """Tests the cleanup mechanism for requests."""
1630 vllm_config = create_vllm_config(
1631 kv_connector="MooncakeConnector", kv_role="kv_producer"
1632 )
1633 with set_current_vllm_config(vllm_config), patch_worker_dependencies():
1634 prefill_connector = MooncakeConnector(
1635 vllm_config,
1636 KVConnectorRole.WORKER,
1637 _make_test_kv_cache_config(),
1638 )
1639 prefill_worker = prefill_connector.connector_worker
1640
1641 # Add an expired request (expire_time is in the past).
1642 prefill_worker.reqs_need_send["tx-expired"] = SendBlockMeta(
1643 p_req_id="p-req-expired",
1644 transfer_id="tx-expired",
1645 local_block_ids=[[1, 2]],
1646 ready=MagicMock(),
1647 expire_time=time.perf_counter() - 100,
1648 )
1649
1650 # Add a non-expired request.
1651 prefill_worker.reqs_need_send["tx-active"] = SendBlockMeta(
1652 p_req_id="p-req-active",
1653 transfer_id="tx-active",
1654 local_block_ids=[[3, 4]],
1655 ready=MagicMock(),
1656 expire_time=time.perf_counter() + 100,
1657 )
1658
1659 finished_reqs = await prefill_worker.fetch_finished_sending_reqs()
1660
1661 assert "p-req-expired" in finished_reqs
1662 assert "p-req-active" not in finished_reqs
1663 assert "tx-expired" not in prefill_worker.reqs_need_send
1664 assert "tx-active" in prefill_worker.reqs_need_send
1665
1666
1667 @pytest.mark.parametrize(
1668 ("layout", "separate_kv_head_groups"),
1669 [
1670 (KVCacheLayout.LBHNC, False),
1671 (KVCacheLayout.BLHNC, False),
1672 (KVCacheLayout.BHLNC, False),
1673 # LHBNC gives each head group its own region; the K/V split doubles the
1674 # head count but the registration shape is driven by the layout.
1675 (KVCacheLayout.LHBNC, False),
1676 (KVCacheLayout.LHBNC, True),
1677 ],
1678 )
1679 def test_register_kv_caches(layout: KVCacheLayout, separate_kv_head_groups: bool):
1680 """Tests the memory registration logic with the underlying Mooncake engine."""
1681 spec = FullAttentionSpec(
1682 block_size=16,
1683 num_kv_heads=4,
1684 head_size=64,
1685 dtype=torch.float16,
1686 num_head_slots=2 if separate_kv_head_groups else None,
1687 state_content_bytes=4 * 64 * 2 if separate_kv_head_groups else None,
1688 )
1689 layer_names = [
1690 "model.layers.0.self_attn",
1691 "model.layers.1.self_attn",
1692 ]
1693 with mooncake_register_worker(_make_test_kv_cache_config()) as (worker, reg):
1694 for layer_name in layer_names:
1695 worker._layer_specs[layer_name] = spec
1696 raw = torch.zeros(2 * 2 * spec.page_size_bytes, dtype=torch.int8)
1697 tensor1, tensor2 = dense_kv_cache_views(raw, spec, 2, 2, layout)
1698 kv_caches = dict(zip(layer_names, (tensor1, tensor2)))
1699 worker.register_kv_caches(kv_caches)
1700
1701 reg.assert_called_once()
1702 registered_ptrs, registered_lens = reg.call_args[0]
1703 assert registered_ptrs == [raw.data_ptr()]
1704 assert registered_lens == [raw.nbytes]
1705
1706 n_blocks = tensor1.shape[0] if tensor1.ndim > 1 else 1
1707 storage_end = raw.data_ptr() + raw.nbytes
1708 for base, stride, kv_len in zip(
1709 worker.kv_caches_base_addr,
1710 worker.block_len_per_layer,
1711 worker.kv_block_len_per_layer,
1712 ):
1713 assert base + (n_blocks - 1) * stride + kv_len <= storage_end
1714
1715 # Expected shape is the layout, not a recomputation of the pack predicate.
1716 kind = {
1717 KVCacheLayout.LBHNC: "page",
1718 KVCacheLayout.BLHNC: "page",
1719 KVCacheLayout.BHLNC: "opaque",
1720 KVCacheLayout.LHBNC: "per_head",
1721 }[layout]
1722 if kind == "opaque":
1723 packed_row = raw.nbytes // n_blocks
1724 assert worker.kv_caches_base_addr == [raw.data_ptr()]
1725 assert worker.block_len_per_layer == [packed_row]
1726 assert worker.kv_block_len_per_layer == [packed_row]
1727 assert worker.registered_layer_names == [layer_names[0]]
1728 elif kind == "per_head":
1729 expected_addrs = [
1730 cache[:, head_idx].data_ptr()
1731 for cache in (tensor1, tensor2)
1732 for head_idx in range(cache.shape[1])
1733 ]
1734 head_block_bytes = tensor1.stride(0) * tensor1.element_size()
1735 assert worker.kv_caches_base_addr == expected_addrs
1736 assert worker.block_len_per_layer == [head_block_bytes] * len(
1737 expected_addrs
1738 )
1739 assert worker.kv_block_len_per_layer == [head_block_bytes] * len(
1740 expected_addrs
1741 )
1742 assert worker.registered_layer_names == [
1743 layer_name
1744 for layer_name in layer_names
1745 for _ in range(tensor1.shape[1])
1746 ]
1747 else:
1748 page_stride = tensor1.stride(0) * tensor1.element_size()
1749 assert len(worker.block_len_per_layer) == len(kv_caches)
1750 assert worker.block_len_per_layer == [page_stride, page_stride]
1751 assert worker.kv_block_len_per_layer == [spec.page_size_bytes] * 2
1752 assert worker.registered_layer_names == list(kv_caches)
1753 assert worker.registered_layer_indices == [0, 1]
1754
1755
1756 def test_register_noncontiguous_packed_allows_matching_pp():
1757 """Same PP on both sides can still register a non-contiguous packed row.
1758
1759 A remote PP mismatch is rejected at send time, once the peer PP size is
1760 known. Rejecting every local PP>1 layout at startup is too strict.
1761 """
1762 spec = FullAttentionSpec(
1763 block_size=16, num_kv_heads=4, head_size=64, dtype=torch.float16
1764 )
1765 layer_names = ["model.layers.0.self_attn", "model.layers.1.self_attn"]
1766 with mooncake_register_worker(_make_test_kv_cache_config()) as (worker, _reg):
1767 worker.pp_size = 2
1768 for layer_name in layer_names:
1769 worker._layer_specs[layer_name] = spec
1770 raw = torch.zeros(2 * 2 * spec.page_size_bytes, dtype=torch.int8)
1771 tensor1, tensor2 = dense_kv_cache_views(raw, spec, 2, 2, KVCacheLayout.BHLNC)
1772 assert not tensor1[0].is_contiguous()
1773 worker.register_kv_caches(dict(zip(layer_names, (tensor1, tensor2))))
1774 assert worker.kv_caches_base_addr == [raw.data_ptr()]
1775 assert worker.region_row_offsets == [0]
1776
1777
1778 def test_register_kv_caches_supports_mixed_mla_and_eagle_shapes():
1779 """Mixed MLA+Eagle caches should register by byte length, not shape."""
1780 with mooncake_register_worker(_make_test_kv_cache_config()) as (worker, reg):
1781 worker.use_mla = True
1782 worker.transfer_topo.is_mla = True
1783 mla_cache = torch.zeros((2, 16, 512), dtype=torch.float16)
1784 eagle_cache = torch.zeros((2, 16, 8, 64), dtype=torch.float16)
1785 kv_caches = {
1786 "model.layers.0.mla_attn": mla_cache,
1787 "model.layers.1.eagle_attn": eagle_cache,
1788 }
1789 worker.register_kv_caches(kv_caches)
1790
1791 reg.assert_called_once()
1792 registered_ptrs, registered_lens = reg.call_args[0]
1793 assert registered_ptrs == [mla_cache.data_ptr(), eagle_cache.data_ptr()]
1794 assert registered_lens == [mla_cache.nbytes, eagle_cache.nbytes]
1795 assert worker.block_len_per_layer == [
1796 mla_cache.nbytes // mla_cache.shape[0],
1797 eagle_cache.nbytes // eagle_cache.shape[0],
1798 ]
1799 assert worker.registered_layer_names == [
1800 "model.layers.0.mla_attn",
1801 "model.layers.1.eagle_attn",
1802 ]
1803 assert worker.registered_layer_indices == [0, 1]
1804 assert len(worker.kv_caches_base_addr) == 2
1805
1806
1807 def test_packed_and_unpacked_region_lengths_fail_homogeneous_tp_handshake():
1808 packed = TransferRegion(
1809 layer_name="model.layers.0.mla_attn",
1810 layer_index=0,
1811 base_addr=0x1000,
1812 block_len=2048,
1813 kv_block_len=2048,
1814 group_index=0,
1815 )
1816 unpacked = TransferRegion(
1817 layer_name="model.layers.0.mla_attn",
1818 layer_index=0,
1819 base_addr=0x1000,
1820 block_len=64,
1821 kv_block_len=64,
1822 group_index=0,
1823 )
1824 err = _validate_asymmetric_region_lengths(
1825 local_regions=[packed],
1826 remote_regions=[unpacked],
1827 local_tp_size=1,
1828 remote_tp_size=1,
1829 producer_cache_replicated=False,
1830 )
1831 assert err is not None
1832 assert "length mismatch" in err
1833
1834 count_err = _validate_asymmetric_region_lengths(
1835 local_regions=[packed],
1836 remote_regions=[unpacked, unpacked],
1837 local_tp_size=1,
1838 remote_tp_size=1,
1839 producer_cache_replicated=False,
1840 )
1841 assert count_err is not None
1842 assert "region counts" in count_err
1843
1844
1845 @pytest.mark.parametrize(
1846 ("num_groups", "expected_group_index"),
1847 [(1, 0), (2, _SHARED_REGION_GROUP_ID)],
1848 )
1849 def test_register_kv_caches_collapses_shared_mla_storage(
1850 num_groups, expected_group_index
1851 ):
1852 """Shared backing collapses to one packed region; two groups mark it -1."""
1853 num_blocks = 4
1854 packed_row = 256
1855 with mooncake_register_worker(
1856 _make_packed_mla_kv_cache_config(num_blocks, num_groups=num_groups)
1857 ) as (worker, reg):
1858 backing = torch.zeros((num_blocks, packed_row), dtype=torch.uint8)
1859 worker.register_kv_caches(
1860 {
1861 "model.layers.0.mla_attn": backing,
1862 "model.layers.1.indexer": backing,
1863 }
1864 )
1865
1866 reg.assert_called_once()
1867 registered_ptrs, registered_lens = reg.call_args[0]
1868 assert registered_ptrs == [backing.data_ptr()]
1869 assert registered_lens == [backing.untyped_storage().nbytes()]
1870 assert worker.kv_caches_base_addr == [backing.data_ptr()]
1871 assert worker.block_len_per_layer == [packed_row]
1872 assert worker.kv_block_len_per_layer == [packed_row]
1873 assert worker.registered_layer_names == ["model.layers.0.mla_attn"]
1874 assert worker.registered_group_indices == [expected_group_index]
1875 if expected_group_index == _SHARED_REGION_GROUP_ID:
1876 assert worker.region_shared_groups == [(0, 1)]
1877
1878
1879 def test_block_ids_for_region_flattens_shared_groups():
1880 ids = [[10, 11], [12]]
1881 assert _block_ids_for_region(ids, 0) == [10, 11]
1882 assert _block_ids_for_region(ids, 1) == [12]
1883 assert _block_ids_for_region(ids, _SHARED_REGION_GROUP_ID, (0, 1)) == [
1884 10,
1885 11,
1886 12,
1887 ]
1888 # A group that does not share the allocation stays out.
1889 assert _block_ids_for_region(
1890 [[10, 11], [12], [99]], _SHARED_REGION_GROUP_ID, (0, 1)
1891 ) == [10, 11, 12]
1892
1893
1894 @pytest.mark.asyncio
1895 @pytest.mark.parametrize(
1896 ("group_index", "local_ids", "remote_ids", "n_blocks"),
1897 [
1898 (0, [[10, 11]], [[20, 21]], 2),
1899 (_SHARED_REGION_GROUP_ID, [[10, 11], [12]], [[20, 21], [22]], 3),
1900 ],
1901 )
1902 async def test_build_transfer_params_sends_packed_region_once(
1903 group_index, local_ids, remote_ids, n_blocks
1904 ):
1905 """Packed region coalesces contiguous blocks, including shared-group flatten."""
1906 worker = MooncakeConnectorWorker.__new__(MooncakeConnectorWorker)
1907 worker.async_zmq_ctx = MagicMock()
1908 worker.is_kv_consumer = True
1909 worker.is_kv_producer = True
1910 worker.tp_rank = 0
1911 worker.tp_size = 1
1912 worker.use_mla = True
1913 worker.kv_cache_config = _make_packed_mla_kv_cache_config(
1914 num_blocks=4, num_groups=len(local_ids)
1915 )
1916 worker._physical_blocks_per_logical_kv_block = 1
1917 worker.transfer_topo = SimpleNamespace(
1918 local_replicates_kv_cache=False,
1919 total_num_kv_heads=1,
1920 )
1921
1922 block_len = 256
1923 shared = (0, 1) if group_index == _SHARED_REGION_GROUP_ID else ()
1924 local_region = _region(
1925 0x1000,
1926 block_len=block_len,
1927 row_offset=0,
1928 layer_name="model.layers.0.mla_attn",
1929 group_index=group_index,
1930 shared_group_ids=shared,
1931 )
1932 remote_region = _region(
1933 0xA000,
1934 block_len=block_len,
1935 row_offset=0,
1936 layer_name="model.layers.0.mla_attn",
1937 group_index=group_index,
1938 shared_group_ids=shared,
1939 )
1940 transfer_id = "xfer-packed"
1941 send_meta = SendBlockMeta(
1942 p_req_id="p-packed",
1943 transfer_id=transfer_id,
1944 local_block_ids=local_ids,
1945 ready=asyncio.Event(),
1946 )
1947 xfer_meta = _xfer_meta([remote_region], {"d-packed": (transfer_id, remote_ids)})
1948
1949 (
1950 src_ptrs,
1951 dst_ptrs,
1952 lengths,
1953 err_reqs,
1954 err_msg,
1955 ) = await worker._build_transfer_params(
1956 ready_reqs=[("d-packed", send_meta)],
1957 agent_meta=xfer_meta,
1958 local_regions=[local_region],
1959 remote_regions=[remote_region],
1960 )
1961
1962 assert err_reqs == []
1963 assert err_msg is None
1964 assert src_ptrs == [0x1000 + 10 * block_len]
1965 assert dst_ptrs == [0xA000 + 20 * block_len]
1966 assert lengths == [n_blocks * block_len]
1967
1968
1969 def test_coalesce_promotes_padding_only_for_a_full_row():
1970 """A full-row merge includes the padding tail; a partial row does not."""
1971 page, row = 1024, 2560
1972 local = [
1973 _region(0, block_len=row, kv_block_len=page, row_offset=0),
1974 _region(page, block_len=row, kv_block_len=page, row_offset=page),
1975 ]
1976 remote = [
1977 _region(100, block_len=row, kv_block_len=page, row_offset=0),
1978 _region(100 + page, block_len=row, kv_block_len=page, row_offset=page),
1979 ]
1980 full_local, full_remote = _coalesce_contiguous_transfer_regions(
1981 local, remote, promote_full_row=True
1982 )
1983 assert len(full_local) == 1
1984 assert full_local[0].kv_block_len == full_remote[0].kv_block_len == row
1985
1986 partial_local, _ = _coalesce_contiguous_transfer_regions(local, remote)
1987 assert partial_local[0].kv_block_len == 2 * page
1988
1989
1990 def test_coalesce_spans_padding_between_pages():
1991 """Unpadded pages merge across the padding gap without a full-row lift."""
1992 row = 2560
1993 merged, _ = _coalesce_contiguous_transfer_regions(
1994 [
1995 _region(0, block_len=row, kv_block_len=1000),
1996 _region(1024, block_len=row, kv_block_len=1000),
1997 ],
1998 [
1999 _region(5000, block_len=row, kv_block_len=1000, row_offset=0),
2000 _region(6024, block_len=row, kv_block_len=1000, row_offset=1024),
2001 ],
2002 )
2003 assert len(merged) == 1
2004 assert merged[0].kv_block_len == 2024
2005 assert merged[0].row_offset == 0
2006
2007
2008 def test_coalesce_promotes_each_group_that_starts_at_row_zero():
2009 """Groups overlay one row, so each row-start run is its own full-row copy."""
2010 page, gap, row = 1000, 1024, 4096
2011
2012 def side(origin: int) -> list[TransferRegion]:
2013 return [
2014 _region(
2015 origin + base,
2016 block_len=row,
2017 kv_block_len=page,
2018 row_offset=offset,
2019 layer_name=f"group-{group}",
2020 layer_index=group,
2021 group_index=group,
2022 )
2023 for group, base, offset in (
2024 (0, 0, 0),
2025 (0, gap, gap),
2026 (1, 0, 0),
2027 (1, gap, gap),
2028 )
2029 ]
2030
2031 merged_local, merged_remote = _coalesce_contiguous_transfer_regions(
2032 side(0), side(8000), promote_full_row=True
2033 )
2034 assert [region.group_index for region in merged_local] == [0, 1]
2035 assert [region.kv_block_len for region in merged_local] == [row, row]
2036 assert [region.kv_block_len for region in merged_remote] == [row, row]
2037 assert merged_local[1].base_addr == 0
2038 assert merged_remote[1].base_addr == 8000
2039
2040
2041 def test_coalesce_does_not_promote_a_mid_row_run():
2042 """A matched PP slice that does not start at row offset 0 stays partial."""
2043 page, row = 1000, 4096
2044 merged, _ = _coalesce_contiguous_transfer_regions(
2045 [
2046 _region(
2047 1024, block_len=row, kv_block_len=page, row_offset=1024, layer_index=2
2048 ),
2049 _region(
2050 2048, block_len=row, kv_block_len=page, row_offset=2048, layer_index=2
2051 ),
2052 ],
2053 [
2054 _region(
2055 9000 + 1024,
2056 block_len=row,
2057 kv_block_len=page,
2058 row_offset=1024,
2059 layer_index=2,
2060 ),
2061 _region(
2062 9000 + 2048,
2063 block_len=row,
2064 kv_block_len=page,
2065 row_offset=2048,
2066 layer_index=2,
2067 ),
2068 ],
2069 promote_full_row=True,
2070 )
2071 assert len(merged) == 1
2072 assert merged[0].row_offset == 1024
2073 assert merged[0].kv_block_len == 1024 + page
2074
2075
2076 def test_opaque_packed_row_is_full_stride_at_offset_zero():
2077 """Branch B is row-sized at offset 0. A page view at offset 0 is not."""
2078 assert _has_opaque_packed_row([0], [4096], [4096])
2079 assert not _has_opaque_packed_row([0, 1024], [1000, 1000], [4096, 4096])
2080 assert not _has_opaque_packed_row([], [4096], [4096])
2081
2082
2083 def test_pp_mismatch_rejects_opaque_row_in_both_directions():
2084 """The smaller PP side can be either the producer or the consumer."""
2085 page_view = dict(
2086 remote_row_offsets=[0, 1024],
2087 remote_kv_block_lens=[1000, 1000],
2088 remote_block_lens=[4096, 4096],
2089 )
2090 opaque_row = dict(
2091 remote_row_offsets=[0],
2092 remote_kv_block_lens=[4096],
2093 remote_block_lens=[4096],
2094 )
2095 # P pp=2, D pp=1, local row is opaque.
2096 assert _pp_mismatch_hides_packed_layers(
2097 2, 1, local_has_opaque_row=True, **page_view
2098 )
2099 # P pp=1, D pp=2, remote row is opaque.
2100 assert _pp_mismatch_hides_packed_layers(
2101 1, 2, local_has_opaque_row=False, **opaque_row
2102 )
2103 # Matching PP still aligns by the shared first layer name.
2104 assert not _pp_mismatch_hides_packed_layers(
2105 2, 2, local_has_opaque_row=True, **opaque_row
2106 )
2107 # Hetero PP with page-contiguous views is not this rejection.
2108 assert not _pp_mismatch_hides_packed_layers(
2109 2, 1, local_has_opaque_row=False, **page_view
2110 )
2111
2112
2113 def test_prepare_transfer_regions_reuses_success_and_error():
2114 """A peer layout is built once. Re-registering drops the cache."""
2115 worker, _, _, _ = _register_sliced_packed_mla([0, 1])
2116 regions = worker._get_transfer_regions(
2117 worker.kv_caches_base_addr,
2118 worker.block_len_per_layer,
2119 worker.kv_block_len_per_layer,
2120 worker.registered_layer_names,
2121 worker.registered_layer_indices,
2122 worker.registered_group_indices,
2123 [tuple(groups) for groups in worker.region_shared_groups],
2124 worker.region_row_offsets,
2125 )
2126 meta = _xfer_meta(
2127 regions,
2128 {},
2129 remote_hostname="peer",
2130 remote_port=1,
2131 remote_tp_size=worker.tp_size,
2132 remote_pp_size=worker.pp_size,
2133 )
2134 calls = 0
2135 original = worker._get_transfer_regions
2136
2137 def counting(*args, **kwargs):
2138 nonlocal calls
2139 calls += 1
2140 return original(*args, **kwargs)
2141
2142 worker._get_transfer_regions = counting
2143 prepared = worker._prepare_transfer_regions(meta)
2144 assert prepared[2] is None
2145 assert calls == 2
2146 assert worker._prepare_transfer_regions(meta) == prepared
2147 assert calls == 2
2148
2149 mismatch = _xfer_meta(
2150 [
2151 _region(
2152 0x2000,
2153 block_len=1024,
2154 row_offset=0,
2155 layer_name="model.layers.9.mla_attn",
2156 layer_index=9,
2157 )
2158 ],
2159 {},
2160 remote_hostname="peer",
2161 remote_port=2,
2162 remote_tp_size=worker.tp_size,
2163 remote_pp_size=worker.pp_size,
2164 )
2165 failed = worker._prepare_transfer_regions(mismatch)
2166 assert failed[2] is not None
2167 assert calls == 4
2168 assert worker._prepare_transfer_regions(mismatch)[2] == failed[2]
2169 assert calls == 4
2170
2171 page = 1024
2172 backing = torch.zeros((4, page * 2), dtype=torch.uint8)
2173 kv_caches = {
2174 "model.layers.0.mla_attn": backing[:, :page],
2175 "model.layers.1.mla_attn": backing[:, page:],
2176 }
2177 worker.is_kv_consumer = True
2178 # The first register built a producer. Its thread patch is already gone,
2179 # so stay on the consumer path and return before the sender thread starts.
2180 with patch.object(worker.engine, "batch_register_memory", return_value=0):
2181 worker.register_kv_caches(kv_caches)
2182 assert worker._prepared_transfer_regions == {}
2183 worker._prepare_transfer_regions(meta)
2184 assert calls == 6
2185
2186
2187 def test_coalesce_keeps_a_run_inside_its_starting_row():
2188 """The row bound is the run start, not the previous slice."""
2189 row = 2500
2190 slices = [
2191 _region(base, block_len=row, kv_block_len=1000) for base in (0, 1000, 2000)
2192 ]
2193 merged, _ = _coalesce_contiguous_transfer_regions(slices, slices)
2194 assert len(merged) == 2
2195 assert merged[0].kv_block_len == 2000
2196 assert merged[1].base_addr == 2000
2197 assert merged[1].kv_block_len == 1000
2198
2199
2200 def _layer_name(idx: int) -> str:
2201 return f"model.layers.{idx}.mla_attn"
2202
2203
2204 def _register_sliced_packed_mla(
2205 layer_idxs: list[int], *, page: int = 1024, num_blocks: int = 4
2206 ):
2207 """Register column slices of one MLA packed row. Returns worker fields."""
2208 spec = MLAAttentionSpec(
2209 block_size=16,
2210 num_kv_heads=1,
2211 head_size=64,
2212 dtype=torch.uint8,
2213 )
2214 kv_cache_config = KVCacheConfig(
2215 num_blocks=num_blocks,
2216 kv_cache_tensors=[],
2217 kv_cache_groups=[
2218 KVCacheGroupSpec([_layer_name(i) for i in layer_idxs], spec),
2219 ],
2220 )
2221 backing = torch.zeros((num_blocks, page * len(layer_idxs)), dtype=torch.uint8)
2222 kv_caches = {
2223 _layer_name(idx): backing[:, i * page : (i + 1) * page]
2224 for i, idx in enumerate(layer_idxs)
2225 }
2226 with mooncake_register_worker(kv_cache_config, kv_role="kv_producer") as (
2227 worker,
2228 _,
2229 ):
2230 worker.register_kv_caches(kv_caches)
2231 regions = worker._get_transfer_regions(
2232 worker.kv_caches_base_addr,
2233 worker.block_len_per_layer,
2234 worker.kv_block_len_per_layer,
2235 worker.registered_layer_names,
2236 worker.registered_layer_indices,
2237 worker.registered_group_indices,
2238 row_offsets=worker.region_row_offsets,
2239 )
2240 return worker, kv_cache_config, regions, backing.data_ptr()
2241
2242
2243 def test_sliced_packed_mla_registers_each_layer_view():
2244 """Each column of a packed MLA row stays its own view, in row order."""
2245 page = 1024
2246 layer_idxs = [0, 1]
2247 with mooncake_register_worker(
2248 KVCacheConfig(
2249 num_blocks=4,
2250 kv_cache_tensors=[],
2251 kv_cache_groups=[
2252 KVCacheGroupSpec(
2253 [_layer_name(i) for i in layer_idxs],
2254 MLAAttentionSpec(
2255 block_size=16,
2256 num_kv_heads=1,
2257 head_size=64,
2258 dtype=torch.uint8,
2259 ),
2260 )
2261 ],
2262 ),
2263 kv_role="kv_producer",
2264 ) as (worker, reg):
2265 backing = torch.zeros((4, page * len(layer_idxs)), dtype=torch.uint8)
2266 worker.register_kv_caches(
2267 {
2268 _layer_name(idx): backing[:, i * page : (i + 1) * page]
2269 for i, idx in enumerate(layer_idxs)
2270 }
2271 )
2272 reg.assert_called_once()
2273 assert reg.call_args[0][0] == [backing.data_ptr()]
2274 assert worker.region_row_offsets == [0, page]
2275
2276
2277 async def _copy_packed_slices(
2278 p_layers: list[int],
2279 d_layers: list[int],
2280 *,
2281 allow_partial: bool,
2282 local_ids: list[list[int]],
2283 remote_ids: list[list[int]],
2284 page: int = 1024,
2285 ):
2286 p_worker, p_config, p_regions, p_base = _register_sliced_packed_mla(
2287 p_layers, page=page
2288 )
2289 _, _, d_regions, d_base = _register_sliced_packed_mla(d_layers, page=page)
2290 aligned_p, aligned_d, err = _align_transfer_regions(
2291 p_regions, d_regions, allow_partial_layers=allow_partial
2292 )
2293 assert err is None
2294 coalesced_p, coalesced_d = _coalesce_contiguous_transfer_regions(
2295 aligned_p, aligned_d
2296 )
2297 transfer_id = "xfer-packed-slices"
2298 send_meta = SendBlockMeta(
2299 p_req_id="p-slices",
2300 transfer_id=transfer_id,
2301 local_block_ids=local_ids,
2302 ready=asyncio.Event(),
2303 )
2304 p_worker.kv_cache_config = p_config
2305 src, dst, lengths, err_reqs, err_msg = await p_worker._build_transfer_params(
2306 ready_reqs=[("d-slices", send_meta)],
2307 agent_meta=_xfer_meta(coalesced_d, {"d-slices": (transfer_id, remote_ids)}),
2308 local_regions=coalesced_p,
2309 remote_regions=coalesced_d,
2310 )
2311 return SimpleNamespace(
2312 p_regions=p_regions,
2313 d_regions=d_regions,
2314 coalesced_p=coalesced_p,
2315 coalesced_d=coalesced_d,
2316 p_base=p_base,
2317 d_base=d_base,
2318 src=src,
2319 dst=dst,
2320 lengths=lengths,
2321 err_reqs=err_reqs,
2322 err_msg=err_msg,
2323 )
2324
2325
2326 @pytest.mark.asyncio
2327 async def test_packed_hetero_pp_keeps_shared_layers():
2328 """P-PP4/D-PP2 copies the shared MLA span, not either side's full row."""
2329 page = 1024
2330 copied = await _copy_packed_slices(
2331 [2, 3],
2332 [0, 1, 2, 3],
2333 allow_partial=True,
2334 local_ids=[[10, 11]],
2335 remote_ids=[[10, 11]],
2336 page=page,
2337 )
2338 assert [region.layer_index for region in copied.p_regions] == [2, 3]
2339 assert [region.kv_block_len for region in copied.p_regions] == [page, page]
2340 assert copied.p_regions[0].block_len == 2 * page
2341 assert copied.d_regions[0].block_len == 4 * page
2342 assert len(copied.coalesced_p) == 1
2343 assert copied.coalesced_p[0].kv_block_len == 2 * page
2344 assert copied.coalesced_d[0].base_addr == copied.d_base + 2 * page
2345 assert copied.err_reqs == []
2346 assert copied.err_msg is None
2347 assert copied.lengths == [2 * page, 2 * page]
2348 assert copied.src == [
2349 copied.p_base + 10 * (2 * page),
2350 copied.p_base + 11 * (2 * page),
2351 ]
2352 assert copied.dst == [
2353 copied.d_base + 2 * page + 10 * (4 * page),
2354 copied.d_base + 2 * page + 11 * (4 * page),
2355 ]
2356
2357
2358 @pytest.mark.asyncio
2359 async def test_packed_matching_layers_coalesce_to_one_copy():
2360 """Identical packed layer sets still emit one copy for contiguous blocks."""
2361 page = 1024
2362 copied = await _copy_packed_slices(
2363 [0, 1],
2364 [0, 1],
2365 allow_partial=False,
2366 local_ids=[[10, 11]],
2367 remote_ids=[[20, 21]],
2368 page=page,
2369 )
2370 assert copied.err_reqs == []
2371 assert copied.err_msg is None
2372 assert len(copied.coalesced_p) == 1
2373 assert copied.coalesced_p[0].kv_block_len == copied.coalesced_p[0].block_len
2374 block_len = 2 * page
2375 assert copied.src == [copied.p_base + 10 * block_len]
2376 assert copied.dst == [copied.d_base + 20 * block_len]
2377 assert copied.lengths == [2 * block_len]
2378
2379
2380 @pytest.mark.asyncio
2381 @patch(
2382 "vllm.distributed.kv_transfer.kv_connector.v1.mooncake."
2383 "mooncake_connector.TransferEngine",
2384 FakeMooncakeWrapper,
2385 )
2386 @pytest.mark.parametrize("d_tp_size", [1, 4], ids=["p_tp2_d_tp1", "p_tp2_d_tp4"])
2387 async def test_kv_producer_heterogeneous_tp(monkeypatch, d_tp_size):
2388 """Tests heterogeneous TP support in the producer transfer path.
2389
2390 Verifies correct pointer and offset calculation when producer TP=2
2391 sends to consumer with TP=1 (P>D) or TP=4 (P<D).
2392
2393 Parametrized cases:
2394 - P TP=2 > D TP=1: one D rank receives; dst_offset based on P rank
2395 - P TP=2 < D TP=4: two D ranks receive; src_offset based on D rank
2396 """
2397 P_TP_SIZE = 2
2398 P_TP_RANK = 0
2399 LOCAL_BLOCK_LEN = 4096
2400
2401 local_block_len = LOCAL_BLOCK_LEN
2402 remote_block_len = LOCAL_BLOCK_LEN * P_TP_SIZE // d_tp_size
2403
2404 monkeypatch.setenv("VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT", "5")
2405 vllm_config = create_vllm_config(
2406 kv_connector="MooncakeConnector", kv_role="kv_producer"
2407 )
2408
2409 with set_current_vllm_config(vllm_config), patch_worker_dependencies():
2410 prefill_connector = MooncakeConnector(
2411 vllm_config,
2412 KVConnectorRole.WORKER,
2413 _make_test_kv_cache_config(),
2414 )
2415 prefill_worker = prefill_connector.connector_worker
2416
2417 # Override TP rank/size to simulate P TP=2
2418 prefill_worker.tp_rank = P_TP_RANK
2419 prefill_worker.tp_size = P_TP_SIZE
2420 prefill_worker._tp_size[prefill_worker.engine_id] = P_TP_SIZE
2421 prefill_worker.transfer_topo.tp_rank = P_TP_RANK
2422 prefill_worker.transfer_topo.tp_size = P_TP_SIZE
2423
2424 prefill_worker.kv_caches_base_addr = [0x1000]
2425 prefill_worker.block_len_per_layer = [local_block_len]
2426 prefill_worker.kv_block_len_per_layer = [local_block_len]
2427 prefill_worker.registered_layer_names = ["model.layers.0.self_attn"]
2428 prefill_worker.registered_layer_indices = [0]
2429
2430 origin_sender_loop = prefill_worker.sender_loop
2431 prefill_worker.sender_loop = asyncio.get_event_loop()
2432
2433 transfer_id = "xfer-hetero-1"
2434 local_block_ids = [[10, 11]]
2435 send_meta = SendBlockMeta(
2436 p_req_id="p-req-h1",
2437 transfer_id=transfer_id,
2438 local_block_ids=local_block_ids,
2439 ready=asyncio.Event(),
2440 )
2441 prefill_worker.reqs_need_send[transfer_id] = send_meta
2442 send_meta.ready.set()
2443
2444 # Compute target D ranks using the production code path
2445 target_d_ranks = prefill_worker.transfer_topo.handshake_target_ranks(d_tp_size)
2446
2447 mock_socket = AsyncMock(spec=zmq.asyncio.Socket)
2448 mock_socket.send_multipart = AsyncMock()
2449 identity = b"consumer-hetero"
2450
2451 # Assign different remote block IDs per D rank (nested per-group)
2452 d_rank_remote_blocks = {
2453 rank: [[20 + i * 10, 21 + i * 10]] for i, rank in enumerate(target_d_ranks)
2454 }
2455
2456 with patch.object(
2457 prefill_worker, "_send_blocks", return_value=0
2458 ) as mock_send_blocks:
2459 for d_rank in target_d_ranks:
2460 remote_block_ids = d_rank_remote_blocks[d_rank]
2461 xfer_meta = _xfer_meta(
2462 [
2463 _region(
2464 0x2000,
2465 block_len=remote_block_len,
2466 row_offset=-1,
2467 layer_name="model.layers.0.self_attn",
2468 layer_index=0,
2469 )
2470 ],
2471 {
2472 f"d-req-h1-r{d_rank}": (
2473 transfer_id,
2474 remote_block_ids,
2475 )
2476 },
2477 remote_tp_size=d_tp_size,
2478 remote_tp_rank=d_rank,
2479 )
2480
2481 mock_send_blocks.reset_mock()
2482 mock_socket.send_multipart.reset_mock()
2483
2484 await prefill_worker.send_kv_to_decode(identity, mock_socket, xfer_meta)
2485
2486 # Verify _send_blocks was called
2487 mock_send_blocks.assert_called_once()
2488 call_args = mock_send_blocks.call_args[0]
2489 src_ptrs = call_args[1]
2490 dst_ptrs = call_args[2]
2491 lengths = call_args[3]
2492
2493 # Flatten nested per-group block IDs for assertions
2494 flat_local = [b for g in local_block_ids for b in g]
2495 flat_remote = [b for g in remote_block_ids for b in g]
2496 num_blocks = len(flat_local)
2497
2498 assert len(src_ptrs) == num_blocks
2499 assert len(dst_ptrs) == num_blocks
2500 assert len(lengths) == num_blocks
2501
2502 if d_tp_size <= P_TP_SIZE:
2503 tp_ratio = P_TP_SIZE // d_tp_size
2504 expected_src_off = 0
2505 expected_dst_off = (P_TP_RANK % tp_ratio) * local_block_len
2506 expected_xfer_len = local_block_len
2507 else:
2508 ratio_abs = d_tp_size // P_TP_SIZE
2509 expected_src_off = (d_rank % ratio_abs) * remote_block_len
2510 expected_dst_off = 0
2511 expected_xfer_len = remote_block_len
2512
2513 local_region_base = 0x1000
2514 remote_region_base = 0x2000
2515 for blk_idx, (lblk, rblk) in enumerate(zip(flat_local, flat_remote)):
2516 assert src_ptrs[blk_idx] == (
2517 local_region_base + lblk * local_block_len + expected_src_off
2518 )
2519 assert dst_ptrs[blk_idx] == (
2520 remote_region_base + rblk * remote_block_len + expected_dst_off
2521 )
2522 assert lengths[blk_idx] == expected_xfer_len
2523
2524 # Verify successful response sent back to consumer
2525 mock_socket.send_multipart.assert_called_once()
2526 _, sent_payload = mock_socket.send_multipart.call_args[0][0]
2527 response = prefill_worker._xfer_resp_decoder.decode(sent_payload)
2528 assert response.status == MooncakeXferResponseStatus.FINISH
2529 assert response.ok_reqs == [f"d-req-h1-r{d_rank}"]
2530
2531 # After serving all D ranks, the request should be complete
2532 assert transfer_id not in prefill_worker.reqs_need_send
2533 assert "p-req-h1" in prefill_worker.finished_sending_reqs
2534
2535 prefill_worker.sender_loop = origin_sender_loop
2536 prefill_worker.shutdown()