| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | from typing import Any |
| 5 | |
| 6 | import msgspec |
| 7 | import pytest |
| 8 | |
| 9 | from vllm.distributed.kv_events import BlockRemoved, BlockStored |
| 10 | |
| 11 | # Minimal ExternalBlockHash for testing (bytes are a valid ExternalBlockHash). |
| 12 | _FAKE_HASH: bytes = b"\xab" * 32 |
| 13 | |
| 14 | |
| 15 | class _LegacyBlockStored( |
| 16 | msgspec.Struct, |
| 17 | omit_defaults=True, # type: ignore[call-arg] |
| 18 | gc=False, # type: ignore[call-arg] |
| 19 | tag="BlockStored", # type: ignore[call-arg] |
| 20 | ): |
| 21 | """BlockStored wire schema before locality was added.""" |
| 22 | |
| 23 | block_hashes: list[bytes] |
| 24 | parent_block_hash: bytes | None |
| 25 | token_ids: list[int] |
| 26 | block_size: int |
| 27 | lora_id: int | None |
| 28 | medium: str | None |
| 29 | lora_name: str | None |
| 30 | extra_keys: list[tuple[Any, ...] | None] | None = None |
| 31 | group_idx: int | None = None |
| 32 | kv_cache_spec_kind: str | None = None |
| 33 | kv_cache_spec_sliding_window: int | None = None |
| 34 | |
| 35 | |
| 36 | class _LegacyBlockRemoved( |
| 37 | msgspec.Struct, |
| 38 | omit_defaults=True, # type: ignore[call-arg] |
| 39 | gc=False, # type: ignore[call-arg] |
| 40 | tag="BlockRemoved", # type: ignore[call-arg] |
| 41 | ): |
| 42 | """BlockRemoved wire schema before locality was added.""" |
| 43 | |
| 44 | block_hashes: list[bytes] |
| 45 | medium: str | None |
| 46 | group_idx: int | None = None |
| 47 | |
| 48 | |
| 49 | class _PreSessionBlockStored( |
| 50 | msgspec.Struct, |
| 51 | omit_defaults=True, # type: ignore[call-arg] |
| 52 | gc=False, # type: ignore[call-arg] |
| 53 | tag="BlockStored", # type: ignore[call-arg] |
| 54 | ): |
| 55 | """BlockStored wire schema before session_id was added.""" |
| 56 | |
| 57 | block_hashes: list[bytes] |
| 58 | parent_block_hash: bytes | None |
| 59 | token_ids: list[int] |
| 60 | block_size: int |
| 61 | lora_id: int | None |
| 62 | medium: str | None |
| 63 | lora_name: str | None |
| 64 | extra_keys: list[tuple[Any, ...] | None] | None = None |
| 65 | group_idx: int | None = None |
| 66 | kv_cache_spec_kind: str | None = None |
| 67 | kv_cache_spec_sliding_window: int | None = None |
| 68 | locality: str | None = None |
| 69 | |
| 70 | |
| 71 | def _make_block_stored( |
| 72 | group_idx: int | None = None, |
| 73 | kv_cache_spec_sliding_window: int | None = None, |
| 74 | locality: str | None = None, |
| 75 | session_id: str | None = None, |
| 76 | ) -> BlockStored: |
| 77 | return BlockStored( |
| 78 | block_hashes=[_FAKE_HASH], |
| 79 | parent_block_hash=None, |
| 80 | token_ids=[1, 2, 3, 4], |
| 81 | block_size=4, |
| 82 | lora_id=None, |
| 83 | medium="GPU", |
| 84 | lora_name=None, |
| 85 | group_idx=group_idx, |
| 86 | kv_cache_spec_sliding_window=kv_cache_spec_sliding_window, |
| 87 | locality=locality, |
| 88 | session_id=session_id, |
| 89 | ) |
| 90 | |
| 91 | |
| 92 | def _make_block_removed( |
| 93 | group_idx: int | None = None, |
| 94 | locality: str | None = None, |
| 95 | ) -> BlockRemoved: |
| 96 | return BlockRemoved( |
| 97 | block_hashes=[_FAKE_HASH], |
| 98 | medium="GPU", |
| 99 | group_idx=group_idx, |
| 100 | locality=locality, |
| 101 | ) |
| 102 | |
| 103 | |
| 104 | def test_block_stored_default_group_idx_is_none(): |
| 105 | """group_idx defaults to None when not provided.""" |
| 106 | event = _make_block_stored() |
| 107 | assert event.group_idx is None |
| 108 | |
| 109 | |
| 110 | def test_block_removed_default_group_idx_is_none(): |
| 111 | """group_idx defaults to None when not provided.""" |
| 112 | event = _make_block_removed() |
| 113 | assert event.group_idx is None |
| 114 | |
| 115 | |
| 116 | @pytest.mark.parametrize("group_idx", [1, 2, 3]) |
| 117 | def test_block_stored_hash_differs_by_group_idx(group_idx: int): |
| 118 | """BlockStored events that differ only in group_idx must hash differently.""" |
| 119 | other_group_idx = group_idx + 1 |
| 120 | event_a = _make_block_stored(group_idx=group_idx) |
| 121 | event_b = _make_block_stored(group_idx=other_group_idx) |
| 122 | assert hash(event_a) != hash(event_b) |
| 123 | |
| 124 | |
| 125 | def test_block_stored_hash_same_for_equal_group_idx(): |
| 126 | """Two BlockStored events with identical fields produce the same hash.""" |
| 127 | event_a = _make_block_stored(group_idx=1) |
| 128 | event_b = _make_block_stored(group_idx=1) |
| 129 | assert hash(event_a) == hash(event_b) |
| 130 | |
| 131 | |
| 132 | def test_block_stored_hash_differs_by_session_id(): |
| 133 | event_a = _make_block_stored(session_id="session-a") |
| 134 | event_b = _make_block_stored(session_id="session-b") |
| 135 | assert hash(event_a) != hash(event_b) |
| 136 | |
| 137 | |
| 138 | @pytest.mark.parametrize("group_idx", [1, 2, 3]) |
| 139 | def test_block_removed_hash_differs_by_group_idx(group_idx: int): |
| 140 | """BlockRemoved events that differ only in group_idx must hash differently.""" |
| 141 | other_group_idx = group_idx + 1 |
| 142 | event_a = _make_block_removed(group_idx=group_idx) |
| 143 | event_b = _make_block_removed(group_idx=other_group_idx) |
| 144 | assert hash(event_a) != hash(event_b) |
| 145 | |
| 146 | |
| 147 | def test_block_removed_hash_same_for_equal_group_idx(): |
| 148 | """Two BlockRemoved events with identical fields produce the same hash.""" |
| 149 | event_a = _make_block_removed(group_idx=1) |
| 150 | event_b = _make_block_removed(group_idx=1) |
| 151 | assert hash(event_a) == hash(event_b) |
| 152 | |
| 153 | |
| 154 | def test_block_stored_hash_differs_by_sliding_window(): |
| 155 | event_a = _make_block_stored(group_idx=1, kv_cache_spec_sliding_window=128) |
| 156 | event_b = _make_block_stored(group_idx=1, kv_cache_spec_sliding_window=256) |
| 157 | assert hash(event_a) != hash(event_b) |
| 158 | |
| 159 | |
| 160 | @pytest.mark.parametrize( |
| 161 | ("event_a", "event_b"), |
| 162 | [ |
| 163 | ( |
| 164 | _make_block_stored(locality="LOCAL"), |
| 165 | _make_block_stored(locality="REMOTE"), |
| 166 | ), |
| 167 | ( |
| 168 | _make_block_removed(locality="LOCAL"), |
| 169 | _make_block_removed(locality="REMOTE"), |
| 170 | ), |
| 171 | ], |
| 172 | ) |
| 173 | def test_event_hash_differs_by_locality( |
| 174 | event_a: BlockStored | BlockRemoved, |
| 175 | event_b: BlockStored | BlockRemoved, |
| 176 | ): |
| 177 | assert hash(event_a) != hash(event_b) |
| 178 | |
| 179 | |
| 180 | def test_block_stored_locality_is_wire_compatible(): |
| 181 | legacy = _LegacyBlockStored( |
| 182 | block_hashes=[_FAKE_HASH], |
| 183 | parent_block_hash=None, |
| 184 | token_ids=[1, 2, 3, 4], |
| 185 | block_size=4, |
| 186 | lora_id=None, |
| 187 | medium="GPU", |
| 188 | lora_name=None, |
| 189 | group_idx=2, |
| 190 | kv_cache_spec_sliding_window=128, |
| 191 | ) |
| 192 | legacy_payload = msgspec.msgpack.encode(legacy) |
| 193 | assert ( |
| 194 | msgspec.msgpack.encode( |
| 195 | _make_block_stored( |
| 196 | group_idx=2, |
| 197 | kv_cache_spec_sliding_window=128, |
| 198 | ) |
| 199 | ) |
| 200 | == legacy_payload |
| 201 | ) |
| 202 | assert msgspec.msgpack.decode(legacy_payload, type=BlockStored).locality is None |
| 203 | new_payload = msgspec.msgpack.encode(_make_block_stored(locality="LOCAL")) |
| 204 | assert msgspec.msgpack.decode(new_payload)["locality"] == "LOCAL" |
| 205 | assert msgspec.msgpack.decode(new_payload, type=_LegacyBlockStored).medium == "GPU" |
| 206 | |
| 207 | |
| 208 | def test_block_removed_locality_is_wire_compatible(): |
| 209 | legacy = _LegacyBlockRemoved(block_hashes=[_FAKE_HASH], medium="GPU") |
| 210 | legacy_payload = msgspec.msgpack.encode(legacy) |
| 211 | assert msgspec.msgpack.encode(_make_block_removed()) == legacy_payload |
| 212 | assert msgspec.msgpack.decode(legacy_payload, type=BlockRemoved).locality is None |
| 213 | new_payload = msgspec.msgpack.encode(_make_block_removed(locality="REMOTE")) |
| 214 | assert msgspec.msgpack.decode(new_payload)["locality"] == "REMOTE" |
| 215 | assert msgspec.msgpack.decode(new_payload, type=_LegacyBlockRemoved).medium == "GPU" |
| 216 | |
| 217 | |
| 218 | def test_block_stored_session_id_is_wire_compatible(): |
| 219 | pre_session = _PreSessionBlockStored( |
| 220 | block_hashes=[_FAKE_HASH], |
| 221 | parent_block_hash=None, |
| 222 | token_ids=[1, 2, 3, 4], |
| 223 | block_size=4, |
| 224 | lora_id=None, |
| 225 | medium="GPU", |
| 226 | lora_name=None, |
| 227 | group_idx=2, |
| 228 | kv_cache_spec_sliding_window=128, |
| 229 | locality="LOCAL", |
| 230 | ) |
| 231 | pre_session_payload = msgspec.msgpack.encode(pre_session) |
| 232 | assert ( |
| 233 | msgspec.msgpack.encode( |
| 234 | _make_block_stored( |
| 235 | group_idx=2, |
| 236 | kv_cache_spec_sliding_window=128, |
| 237 | locality="LOCAL", |
| 238 | ) |
| 239 | ) |
| 240 | == pre_session_payload |
| 241 | ) |
| 242 | assert ( |
| 243 | msgspec.msgpack.decode(pre_session_payload, type=BlockStored).session_id is None |
| 244 | ) |
| 245 | |
| 246 | new_payload = msgspec.msgpack.encode(_make_block_stored(session_id="session-1")) |
| 247 | assert msgspec.msgpack.decode(new_payload)["session_id"] == "session-1" |
| 248 | assert ( |
| 249 | msgspec.msgpack.decode(new_payload, type=_PreSessionBlockStored).medium == "GPU" |
| 250 | ) |