Download code/models/common/tests/llm_runtime/test_paged_kv_cache.py from tt-hous/clef: direct link, hf CLI and curl.
- Browser
- Download file 14.3 kB
-
https://huggingface.co/tt-hous/clef/resolve/main/code/models/common/tests/llm_runtime/test_paged_kv_cache.py
- Command line
-
hf download hf://tt-hous/clef/code/models/common/tests/llm_runtime/test_paged_kv_cache.py
-
curl -L -o test_paged_kv_cache.py https://huggingface.co/tt-hous/clef/resolve/main/code/models/common/tests/llm_runtime/test_paged_kv_cache.py
14.3 kB
| # SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC | |
| # SPDX-License-Identifier: Apache-2.0 | |
| from dataclasses import replace | |
| from types import SimpleNamespace | |
| import pytest | |
| import torch | |
| import ttnn | |
| from models.common.llm_runtime.config import PagedKVCacheConfig | |
| from models.common.llm_runtime.paged_kv_cache import PagedKVCacheManager, torch_dtype_for_ttnn | |
| class FakeMesh: | |
| def __init__(self, num_devices=2): | |
| self._num_devices = num_devices | |
| def get_num_devices(self): | |
| return self._num_devices | |
| class FakeTensor: | |
| def __init__(self, shape, dtype): | |
| self.shape = tuple(shape) | |
| self.dtype = dtype | |
| def volume(self): | |
| result = 1 | |
| for dimension in self.shape: | |
| result *= dimension | |
| return result | |
| def element_size(self): | |
| return 1 if self.dtype in (ttnn.bfloat8_b, ttnn.bfloat4_b) else 2 | |
| class FakeModel: | |
| def __init__(self, dtypes=(ttnn.bfloat8_b, ttnn.bfloat8_b), *, bind_fails=False): | |
| mesh = FakeMesh(2) | |
| blocks = [] | |
| for dtype in dtypes: | |
| attention = SimpleNamespace( | |
| n_kv_heads=8, | |
| head_dim=16, | |
| kv_cache_dtype=dtype, | |
| paged_attention_config=SimpleNamespace(block_size=32, max_num_blocks=8), | |
| ) | |
| blocks.append(SimpleNamespace(attention_config=attention)) | |
| self.config = SimpleNamespace( | |
| block_configs=blocks, | |
| n_layers=len(blocks), | |
| num_devices=2, | |
| mesh_device=mesh, | |
| ) | |
| self.set_calls = [] | |
| self.bound_cache = None | |
| self.bind_fails = bind_fails | |
| def set_kv_cache(self, cache): | |
| self.set_calls.append(cache) | |
| if cache is not None and self.bind_fails: | |
| raise RuntimeError("bind failed") | |
| self.bound_cache = cache | |
| def cache_config(**overrides): | |
| values = { | |
| "block_size": 32, | |
| "max_num_blocks": 8, | |
| "dtype": ttnn.bfloat8_b, | |
| } | |
| values.update(overrides) | |
| return PagedKVCacheConfig(**values) | |
| def fake_allocator(monkeypatch): | |
| allocated = [] | |
| deallocated = [] | |
| # Non-failure TTNN fakes retain overloaded backend keyword options for assertions. | |
| def as_tensor(host_tensor, **kwargs): | |
| tensor = FakeTensor(host_tensor.shape, kwargs["dtype"]) | |
| allocated.append((tensor, host_tensor, kwargs)) | |
| return tensor | |
| monkeypatch.setattr(ttnn, "as_tensor", as_tensor) | |
| monkeypatch.setattr(ttnn, "ReplicateTensorToMesh", lambda mesh: ("replicate", mesh)) | |
| monkeypatch.setattr(ttnn, "deallocate", lambda tensor: deallocated.append(tensor)) | |
| return allocated, deallocated | |
| def test_unresolved_config_accepts_one_resolved_replacement(expect_error): | |
| manager = PagedKVCacheManager(FakeModel(), cache_config()) | |
| resolved = replace(manager.config, num_blocks=4) | |
| manager.configure(resolved) | |
| assert manager.config is resolved | |
| assert manager.config.num_blocks == 4 | |
| with expect_error(RuntimeError, "only once"): | |
| manager.configure(replace(resolved, num_blocks=5)) | |
| def test_resolved_replacement_accepts_model_aligned_external_geometry(expect_error): | |
| model = FakeModel() | |
| manager = PagedKVCacheManager(model, cache_config()) | |
| for block in model.config.block_configs: | |
| block.attention_config.paged_attention_config.block_size = 16 | |
| block.attention_config.paged_attention_config.max_num_blocks = 12 | |
| manager.configure(cache_config(block_size=16, max_num_blocks=12, num_blocks=12)) | |
| assert manager.config.block_size == 16 | |
| assert manager.config.max_num_blocks == manager.config.num_blocks == 12 | |
| other = PagedKVCacheManager(FakeModel(), cache_config()) | |
| with expect_error(ValueError, "dtype"): | |
| other.configure(cache_config(dtype=ttnn.bfloat16, num_blocks=4)) | |
| def test_pre_resolved_config_starts_configured_and_cannot_be_replaced(expect_error): | |
| manager = PagedKVCacheManager(FakeModel(), cache_config(num_blocks=4)) | |
| with expect_error(RuntimeError, "only once"): | |
| manager.configure(cache_config(num_blocks=5)) | |
| def test_model_paged_attention_policy_and_uniform_dtype_are_validated(expect_error): | |
| model = FakeModel() | |
| model.config.block_configs[1].attention_config.paged_attention_config.block_size = 16 | |
| with expect_error(ValueError, "block_size"): | |
| PagedKVCacheManager(model, cache_config()) | |
| with expect_error(ValueError, "model-owned dtype"): | |
| PagedKVCacheManager(FakeModel(), cache_config(dtype=ttnn.bfloat16)) | |
| def test_vllm_torch_dtype_mapping_includes_quantized_surrogate(expect_error): | |
| manager = PagedKVCacheManager(FakeModel(), cache_config()) | |
| assert torch_dtype_for_ttnn(ttnn.bfloat8_b) == torch.bfloat16 | |
| manager.validate_vllm_cache_spec(block_size=32, dtype=torch.bfloat16, num_blocks=8) | |
| with expect_error(ValueError, "incompatible"): | |
| manager.validate_vllm_cache_spec(block_size=32, dtype=torch.float32) | |
| with expect_error(ValueError, "block_size"): | |
| manager.validate_vllm_cache_spec(block_size=16, dtype=torch.bfloat16) | |
| with expect_error(ValueError, "exceeds configured maximum"): | |
| manager.validate_vllm_cache_spec(block_size=32, dtype=torch.bfloat16, num_blocks=9) | |
| def test_nonuniform_model_dtypes_remain_per_layer(): | |
| manager = PagedKVCacheManager( | |
| FakeModel(dtypes=(ttnn.bfloat8_b, ttnn.bfloat16)), | |
| cache_config(dtype=ttnn.bfloat8_b), | |
| ) | |
| assert manager.per_layer_dtypes == (ttnn.bfloat8_b, ttnn.bfloat16) | |
| manager.validate_vllm_cache_spec(block_size=32, dtype=torch.bfloat16) | |
| def test_allocate_derives_shapes_and_dtypes_binds_exact_borrowed_handle(fake_allocator, expect_error): | |
| allocated, _ = fake_allocator | |
| model = FakeModel(dtypes=(ttnn.bfloat8_b, ttnn.bfloat16)) | |
| manager = PagedKVCacheManager(model, cache_config(num_blocks=4)) | |
| cache = manager.allocate() | |
| assert cache is model.bound_cache | |
| assert manager.cache_shapes == ((4, 4, 32, 16), (4, 4, 32, 16)) | |
| assert [entry[0].dtype for entry in allocated] == [ | |
| ttnn.bfloat8_b, | |
| ttnn.bfloat8_b, | |
| ttnn.bfloat16, | |
| ttnn.bfloat16, | |
| ] | |
| assert all(tuple(entry[1].shape) == (4, 4, 32, 16) for entry in allocated) | |
| assert len({id(entry[1]) for entry in allocated}) == 1 | |
| assert all(entry[2]["memory_config"] == ttnn.DRAM_MEMORY_CONFIG for entry in allocated) | |
| assert all(entry[2]["cache_file_name"] is None for entry in allocated) | |
| manager.validate_borrowed_handle(cache) | |
| with expect_error(ValueError, "exact manager-owned"): | |
| manager.validate_borrowed_handle([pair[:] for pair in cache]) | |
| assert manager.bound_context.config is manager.config | |
| assert manager.bound_context.tensors[0][0] is cache[0][0] | |
| def test_allocate_reuses_legacy_cache_files_when_dtype_is_unambiguous(fake_allocator, tmp_path): | |
| allocated, _ = fake_allocator | |
| model = FakeModel() | |
| model.model_args = SimpleNamespace(model_cache_path=tmp_path) | |
| manager = PagedKVCacheManager(model, cache_config(num_blocks=4)) | |
| manager.allocate() | |
| shape = (4, 4, 32, 16) | |
| expected = [ | |
| tmp_path / f"empty_kcache_paged_attention{shape}", | |
| tmp_path / f"empty_vcache_paged_attention{shape}", | |
| ] | |
| assert [entry[2]["cache_file_name"] for entry in allocated] == expected * 2 | |
| assert len({id(entry[1]) for entry in allocated}) == 1 | |
| def test_allocate_avoids_cache_file_collision_for_nonuniform_device_dtypes(fake_allocator, tmp_path): | |
| allocated, _ = fake_allocator | |
| model = FakeModel(dtypes=(ttnn.bfloat8_b, ttnn.bfloat16)) | |
| model.model_args = SimpleNamespace(model_cache_path=tmp_path) | |
| manager = PagedKVCacheManager(model, cache_config(num_blocks=4)) | |
| manager.allocate() | |
| assert all(entry[2]["cache_file_name"] is None for entry in allocated) | |
| def test_allocation_requires_resolved_capacity_and_happens_once(fake_allocator, expect_error): | |
| model = FakeModel() | |
| manager = PagedKVCacheManager(model, cache_config()) | |
| with expect_error(RuntimeError, "must be resolved"): | |
| manager.allocate() | |
| manager.configure(replace(manager.config, num_blocks=4)) | |
| cache = manager.allocate() | |
| with expect_error(RuntimeError, "already been allocated"): | |
| manager.allocate() | |
| assert cache is model.bound_cache | |
| def test_partial_allocation_failure_deallocates_created_tensors(monkeypatch, expect_error): | |
| first = FakeTensor((4, 4, 32, 16), ttnn.bfloat8_b) | |
| calls = 0 | |
| deallocated = [] | |
| def fail_second_allocation( | |
| host_tensor, | |
| *, | |
| device, | |
| mesh_mapper, | |
| layout, | |
| memory_config, | |
| dtype, | |
| cache_file_name, | |
| ): | |
| nonlocal calls | |
| calls += 1 | |
| if calls == 2: | |
| raise RuntimeError("allocation failed") | |
| return first | |
| monkeypatch.setattr(ttnn, "as_tensor", fail_second_allocation) | |
| monkeypatch.setattr(ttnn, "ReplicateTensorToMesh", lambda mesh: None) | |
| monkeypatch.setattr(ttnn, "deallocate", lambda tensor: deallocated.append(tensor)) | |
| model = FakeModel() | |
| manager = PagedKVCacheManager(model, cache_config(num_blocks=4)) | |
| with expect_error(RuntimeError, "allocation failed"): | |
| manager.allocate() | |
| assert deallocated == [first] | |
| assert model.set_calls == [] | |
| def test_bind_failure_unbinds_then_deallocates_all_tensors(fake_allocator, expect_error): | |
| allocated, deallocated = fake_allocator | |
| model = FakeModel(bind_fails=True) | |
| manager = PagedKVCacheManager(model, cache_config(num_blocks=4)) | |
| with expect_error(RuntimeError, "bind failed"): | |
| manager.allocate() | |
| assert model.set_calls[-1] is None | |
| assert deallocated == [entry[0] for entry in reversed(allocated)] | |
| assert model.bound_cache is None | |
| def test_release_unbinds_before_deallocating_and_is_idempotent(monkeypatch, expect_error): | |
| operations = [] | |
| model = FakeModel() | |
| original_set = model.set_kv_cache | |
| def set_kv_cache(cache): | |
| operations.append(("bind", cache)) | |
| original_set(cache) | |
| model.set_kv_cache = set_kv_cache | |
| monkeypatch.setattr( | |
| ttnn, | |
| "as_tensor", | |
| lambda host_tensor, **kwargs: FakeTensor(host_tensor.shape, kwargs["dtype"]), | |
| ) | |
| monkeypatch.setattr(ttnn, "ReplicateTensorToMesh", lambda mesh: None) | |
| monkeypatch.setattr(ttnn, "deallocate", lambda tensor: operations.append(("deallocate", tensor))) | |
| manager = PagedKVCacheManager(model, cache_config(num_blocks=4)) | |
| cache = manager.allocate() | |
| operations.clear() | |
| manager.release() | |
| assert operations[0] == ("bind", None) | |
| assert [operation[1] for operation in operations[1:]] == [tensor for pair in cache for tensor in pair] | |
| assert manager.bound_context is None | |
| manager.release() | |
| assert len(operations) == 5 | |
| with expect_error(RuntimeError, "terminal"): | |
| manager.allocate() | |
| def test_borrowed_handle_mutation_cannot_redirect_owned_tensor_release(fake_allocator, expect_error): | |
| allocated, deallocated = fake_allocator | |
| model = FakeModel() | |
| manager = PagedKVCacheManager(model, cache_config(num_blocks=4)) | |
| cache = manager.allocate() | |
| owned_tensors = [entry[0] for entry in allocated] | |
| replacement = FakeTensor(cache[0][0].shape, cache[0][0].dtype) | |
| cache[0][0] = replacement | |
| with expect_error(ValueError, "exact manager-owned K/V tensors"): | |
| manager.validate_borrowed_handle(cache) | |
| manager.release() | |
| assert deallocated == owned_tensors | |
| assert replacement not in deallocated | |
| def test_release_failure_retains_only_failed_tensor_and_retries(monkeypatch, expect_error): | |
| tensors = [] | |
| def as_tensor(host_tensor, **kwargs): | |
| tensor = FakeTensor(host_tensor.shape, kwargs["dtype"]) | |
| tensors.append(tensor) | |
| return tensor | |
| monkeypatch.setattr(ttnn, "as_tensor", as_tensor) | |
| monkeypatch.setattr(ttnn, "ReplicateTensorToMesh", lambda mesh: None) | |
| model = FakeModel() | |
| manager = PagedKVCacheManager(model, cache_config(num_blocks=4)) | |
| manager.allocate() | |
| failed_tensor = tensors[0] | |
| attempts = [] | |
| def fail_once(tensor): | |
| attempts.append(tensor) | |
| if tensor is failed_tensor and attempts.count(tensor) == 1: | |
| raise RuntimeError("deallocate failed") | |
| monkeypatch.setattr(ttnn, "deallocate", fail_once) | |
| with expect_error(RuntimeError, "Failed to deallocate 1"): | |
| manager.release() | |
| assert manager.bound_context is None | |
| assert model.bound_cache is None | |
| assert attempts == tensors | |
| manager.release() | |
| assert attempts.count(failed_tensor) == 2 | |
| assert all(attempts.count(tensor) == 1 for tensor in tensors[1:]) | |
| def test_partial_allocation_cleanup_failure_preserves_tensor_for_release_retry(monkeypatch, expect_error): | |
| tensor = FakeTensor((4, 4, 32, 16), ttnn.bfloat8_b) | |
| allocation_calls = 0 | |
| deallocation_calls = 0 | |
| def fail_second_allocation( | |
| host_tensor, | |
| *, | |
| device, | |
| mesh_mapper, | |
| layout, | |
| memory_config, | |
| dtype, | |
| cache_file_name, | |
| ): | |
| nonlocal allocation_calls | |
| allocation_calls += 1 | |
| if allocation_calls == 2: | |
| raise RuntimeError("allocation failed") | |
| return tensor | |
| def fail_first_deallocation(value): | |
| nonlocal deallocation_calls | |
| deallocation_calls += 1 | |
| if deallocation_calls == 1: | |
| raise RuntimeError("cleanup failed") | |
| monkeypatch.setattr(ttnn, "as_tensor", fail_second_allocation) | |
| monkeypatch.setattr(ttnn, "ReplicateTensorToMesh", lambda mesh: None) | |
| monkeypatch.setattr(ttnn, "deallocate", fail_first_deallocation) | |
| manager = PagedKVCacheManager(FakeModel(), cache_config(num_blocks=4)) | |
| with expect_error(RuntimeError, "allocation failed") as exc_info: | |
| manager.allocate() | |
| assert [str(error) for error in exc_info.value.cleanup_failures] == ["cleanup failed"] | |
| manager.release() | |
| assert deallocation_calls == 2 | |
| with expect_error(RuntimeError, "terminal"): | |
| manager.allocate() | |
| def test_release_before_allocation_is_terminal_and_idempotent(expect_error): | |
| manager = PagedKVCacheManager(FakeModel(), cache_config()) | |
| manager.release() | |
| manager.release() | |
| with expect_error(RuntimeError, "terminal"): | |
| manager.allocate() | |