| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Tests for HF3FS metadata server data structures and allocation logic: |
| 4 | - RankFileMetadata : page allocation / release primitives |
| 5 | - KeyMetadata : per-key rank-page tracking and completion detection |
| 6 | - GlobalMetadataState : coordinated allocation with cache-hit semantics |
| 7 | """ |
| 8 | |
| 9 | import pytest |
| 10 | |
| 11 | from vllm.distributed.kv_transfer.kv_connector.v1.hf3fs.hf3fs_metadata_server import ( |
| 12 | GlobalMetadataState, |
| 13 | KeyMetadata, |
| 14 | RankFileMetadata, |
| 15 | ) |
| 16 | |
| 17 | # =========================================================================== |
| 18 | # TestRankFileMetadata |
| 19 | # =========================================================================== |
| 20 | |
| 21 | |
| 22 | class TestRankFileMetadata: |
| 23 | """Unit tests for RankFileMetadata page allocation primitives.""" |
| 24 | |
| 25 | @pytest.mark.parametrize( |
| 26 | "alloc_count, expected_pages", |
| 27 | [(3, 3), (5, 0)], |
| 28 | ids=["alloc_partial", "alloc_exceeds"], |
| 29 | ) |
| 30 | def test_allocate_pages(self, alloc_count, expected_pages): |
| 31 | """allocate_pages returns correct pages or empty list when insufficient.""" |
| 32 | rank_meta = RankFileMetadata(rank_id=0, num_pages=3, free_pages=list(range(3))) |
| 33 | pages = rank_meta.allocate_pages(alloc_count) |
| 34 | assert len(pages) == expected_pages |
| 35 | if expected_pages > 0: |
| 36 | rank_meta.release_pages(pages) |
| 37 | assert rank_meta.get_free_page_count() == 3 |
| 38 | |
| 39 | def test_release_pages_restores_count(self): |
| 40 | """Releasing allocated pages returns them to the free pool.""" |
| 41 | rank_meta = RankFileMetadata(rank_id=0, num_pages=4, free_pages=list(range(4))) |
| 42 | pages = rank_meta.allocate_pages(2) |
| 43 | assert rank_meta.get_free_page_count() == 2 |
| 44 | rank_meta.release_pages(pages) |
| 45 | assert rank_meta.get_free_page_count() == 4 |
| 46 | |
| 47 | def test_release_pages_no_duplicates(self): |
| 48 | """Releasing the same page twice must not create duplicates.""" |
| 49 | rank_meta = RankFileMetadata(rank_id=0, num_pages=3, free_pages=list(range(3))) |
| 50 | rank_meta.allocate_pages(1) # takes page 0 |
| 51 | rank_meta.release_pages([0]) |
| 52 | rank_meta.release_pages([0]) # second release of the same page |
| 53 | assert rank_meta.get_free_page_count() == 3 |
| 54 | |
| 55 | |
| 56 | # =========================================================================== |
| 57 | # TestKeyMetadata |
| 58 | # =========================================================================== |
| 59 | |
| 60 | |
| 61 | class TestKeyMetadata: |
| 62 | """Unit tests for KeyMetadata completion tracking.""" |
| 63 | |
| 64 | def test_is_complete_false_until_all_ranks(self): |
| 65 | """is_complete() returns True only when all ranks confirmed.""" |
| 66 | key_meta = KeyMetadata(key="k", rank_to_page={}, tp_world_size=2) |
| 67 | assert key_meta.is_complete() is False |
| 68 | key_meta.add_rank_page(0, 5) |
| 69 | assert key_meta.is_complete() is False |
| 70 | key_meta.add_rank_page(1, 10) |
| 71 | assert key_meta.is_complete() is True |
| 72 | |
| 73 | def test_get_rank_page_returns_none_for_missing_rank(self): |
| 74 | """get_rank_page() returns None when the rank has no entry.""" |
| 75 | key_meta = KeyMetadata(key="k", rank_to_page={0: 3}, tp_world_size=2) |
| 76 | assert key_meta.get_rank_page(0) == 3 |
| 77 | assert key_meta.get_rank_page(1) is None |
| 78 | |
| 79 | def test_get_all_pages(self): |
| 80 | """get_all_pages() returns all (rank, page) pairs.""" |
| 81 | key_meta = KeyMetadata(key="k", rank_to_page={0: 1, 1: 2}, tp_world_size=2) |
| 82 | pairs = key_meta.get_all_pages() |
| 83 | assert set(pairs) == {(0, 1), (1, 2)} |
| 84 | |
| 85 | |
| 86 | # =========================================================================== |
| 87 | # TestGlobalMetadataStateAllocation |
| 88 | # =========================================================================== |
| 89 | |
| 90 | |
| 91 | class TestGlobalMetadataStateAllocation: |
| 92 | """Tests for GlobalMetadataState allocation and cache-hit semantics.""" |
| 93 | |
| 94 | def test_uninitialized_rank_raises_on_allocate(self): |
| 95 | """allocate_pages_for_keys raises ValueError for unknown rank.""" |
| 96 | state = GlobalMetadataState() |
| 97 | with pytest.raises((ValueError, Exception)): |
| 98 | state.allocate_pages_for_keys(99, [("key", "")]) |
| 99 | |
| 100 | def test_uninitialized_rank_raises_on_get_locations(self): |
| 101 | """get_key_locations raises ValueError for unknown rank.""" |
| 102 | state = GlobalMetadataState() |
| 103 | with pytest.raises((ValueError, Exception)): |
| 104 | state.get_key_locations(99, ["any_key"]) |
| 105 | |
| 106 | def test_basic_allocation_and_confirm(self): |
| 107 | """Allocating a page and confirming it marks the key as complete.""" |
| 108 | state = GlobalMetadataState() |
| 109 | state.initialize_rank(0, 4) |
| 110 | |
| 111 | results = state.allocate_pages_for_keys(0, [("K", "")]) |
| 112 | assert results["K"] >= 0 |
| 113 | |
| 114 | state.confirm_write_for_keys(0, [("K", results["K"])]) |
| 115 | assert state.batch_key_exists(["K"]) == [True] |
| 116 | locations = state.get_key_locations(0, ["K"]) |
| 117 | assert locations == [results["K"]] |
| 118 | |
| 119 | def test_allocate_pages_cache_hit_does_not_leak_pages(self): |
| 120 | """Cache-hit key must not consume a page from the free pool; |
| 121 | the pre-allocated slot must be returned before reusing the existing page. |
| 122 | """ |
| 123 | state = GlobalMetadataState() |
| 124 | state.initialize_rank(0, 5) # 5 free pages: [0,1,2,3,4] |
| 125 | |
| 126 | # Simulate a key that has already been fully written and confirmed. |
| 127 | state.key_metadata["K_cached"] = KeyMetadata( |
| 128 | key="K_cached", rank_to_page={0: 2}, tp_world_size=1 |
| 129 | ) |
| 130 | |
| 131 | free_before = state.rank_metadata[0].get_free_page_count() # 5 |
| 132 | |
| 133 | results = state.allocate_pages_for_keys(0, [("K_cached", ""), ("K_new", "")]) |
| 134 | |
| 135 | free_after = state.rank_metadata[0].get_free_page_count() |
| 136 | |
| 137 | # Cache-hit key must reuse its existing page. |
| 138 | assert results["K_cached"] == 2, ( |
| 139 | f"Cache-hit key should reuse page 2, got {results['K_cached']}" |
| 140 | ) |
| 141 | # New key must receive a valid page. |
| 142 | assert results["K_new"] >= 0, ( |
| 143 | f"New key should get a valid page, got {results['K_new']}" |
| 144 | ) |
| 145 | # Exactly one page consumed from the free pool. |
| 146 | assert free_before - free_after == 1, ( |
| 147 | f"Expected 1 page consumed, got delta={free_before - free_after}" |
| 148 | ) |
| 149 | |
| 150 | def test_allocate_pages_all_cache_hits_frees_all_slots(self): |
| 151 | """When every key in the batch is a cache hit, no pages are consumed.""" |
| 152 | state = GlobalMetadataState() |
| 153 | state.initialize_rank(0, 5) |
| 154 | |
| 155 | for key, page in (("K1", 0), ("K2", 1)): |
| 156 | state.key_metadata[key] = KeyMetadata( |
| 157 | key=key, rank_to_page={0: page}, tp_world_size=1 |
| 158 | ) |
| 159 | |
| 160 | free_before = state.rank_metadata[0].get_free_page_count() |
| 161 | results = state.allocate_pages_for_keys(0, [("K1", ""), ("K2", "")]) |
| 162 | free_after = state.rank_metadata[0].get_free_page_count() |
| 163 | |
| 164 | assert results["K1"] == 0 |
| 165 | assert results["K2"] == 1 |
| 166 | assert free_after == free_before, ( |
| 167 | f"All-cache-hit batch must not consume free pages; " |
| 168 | f"before={free_before}, after={free_after}" |
| 169 | ) |
| 170 | |
| 171 | def test_allocate_returns_minus_one_when_pool_exhausted(self): |
| 172 | """If the free pool is exhausted, all new keys receive -1.""" |
| 173 | state = GlobalMetadataState() |
| 174 | state.initialize_rank(0, 1) # only 1 free page |
| 175 | |
| 176 | results = state.allocate_pages_for_keys(0, [("K1", ""), ("K2", "")]) |
| 177 | # allocate_pages uses all-or-nothing: 2 needed but only 1 available → [] |
| 178 | assert all(v == -1 for v in results.values()), f"Expected all -1, got {results}" |
| 179 | |
| 180 | def test_confirm_write_releases_pages(self): |
| 181 | """confirm_write_for_keys with pages_to_release returns them to pool.""" |
| 182 | state = GlobalMetadataState() |
| 183 | state.initialize_rank(0, 3) |
| 184 | |
| 185 | results = state.allocate_pages_for_keys(0, [("K", "")]) |
| 186 | page = results["K"] |
| 187 | free_after_alloc = state.rank_metadata[0].get_free_page_count() |
| 188 | |
| 189 | state.confirm_write_for_keys(0, [("K", page)], pages_to_release=[page]) |
| 190 | free_after_release = state.rank_metadata[0].get_free_page_count() |
| 191 | |
| 192 | assert free_after_release == free_after_alloc + 1 |