688 lines
21 KiB
Python
688 lines
21 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Unit tests for lmcache.utils module."""
|
|
|
|
# Standard
|
|
from unittest.mock import patch
|
|
|
|
# Third Party
|
|
import pytest
|
|
import torch
|
|
|
|
# First Party
|
|
from lmcache.utils import (
|
|
CacheEngineKey,
|
|
LayerCacheEngineKey,
|
|
_append_range_or_elements,
|
|
cdiv,
|
|
compress_slot_mapping,
|
|
convert_tokens_to_list,
|
|
decompress_slot_mapping,
|
|
get_version,
|
|
parse_cache_key,
|
|
parse_mixed_slot_mapping,
|
|
round_down,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Math utility functions
|
|
# ============================================================
|
|
class TestCdiv:
|
|
def test_exact_division(self):
|
|
assert cdiv(10, 5) == 2
|
|
|
|
def test_ceiling_division(self):
|
|
assert cdiv(7, 3) == 3
|
|
|
|
def test_one_element(self):
|
|
assert cdiv(1, 1) == 1
|
|
|
|
def test_numerator_smaller_than_denominator(self):
|
|
assert cdiv(1, 3) == 1
|
|
|
|
def test_large_values(self):
|
|
assert cdiv(1000000001, 1000000000) == 2
|
|
|
|
|
|
class TestRoundDown:
|
|
def test_exact_multiple(self):
|
|
assert round_down(10, 5) == 10
|
|
|
|
def test_not_exact_multiple(self):
|
|
assert round_down(7, 3) == 6
|
|
|
|
def test_smaller_than_y(self):
|
|
assert round_down(2, 5) == 0
|
|
|
|
def test_zero(self):
|
|
assert round_down(0, 5) == 0
|
|
|
|
|
|
# ============================================================
|
|
# Slot mapping compression / decompression
|
|
# ============================================================
|
|
class TestCompressSlotMapping:
|
|
def test_empty_list(self):
|
|
assert compress_slot_mapping([]) == []
|
|
|
|
def test_single_element(self):
|
|
assert compress_slot_mapping([5]) == [5]
|
|
|
|
def test_two_consecutive(self):
|
|
# Two consecutive elements should NOT be compressed
|
|
assert compress_slot_mapping([3, 4]) == [3, 4]
|
|
|
|
def test_three_or_more_consecutive(self):
|
|
assert compress_slot_mapping([1, 2, 3]) == [[1, 3]]
|
|
assert compress_slot_mapping([1, 2, 3, 4, 5]) == [[1, 5]]
|
|
|
|
def test_mixed(self):
|
|
result = compress_slot_mapping([1, 2, 3, 4, 5, 7, 8])
|
|
assert result == [[1, 5], 7, 8]
|
|
|
|
def test_non_consecutive_order_preserved(self):
|
|
# 1,2 is only 2 consecutive -> not compressed
|
|
# 2,4 not consecutive -> separate
|
|
result = compress_slot_mapping([5, 3, 1, 2, 4])
|
|
assert result == [5, 3, 1, 2, 4]
|
|
|
|
def test_multiple_ranges(self):
|
|
result = compress_slot_mapping([1, 2, 3, 4, 5, 9, 10, 11, 12])
|
|
assert result == [[1, 5], [9, 12]]
|
|
|
|
def test_all_non_consecutive(self):
|
|
result = compress_slot_mapping([1, 3, 5, 7])
|
|
assert result == [1, 3, 5, 7]
|
|
|
|
|
|
class TestAppendRangeOrElements:
|
|
def test_single_element(self):
|
|
result = []
|
|
_append_range_or_elements(result, 5, 5)
|
|
assert result == [5]
|
|
|
|
def test_two_elements(self):
|
|
result = []
|
|
_append_range_or_elements(result, 5, 6)
|
|
assert result == [5, 6]
|
|
|
|
def test_three_or_more_elements(self):
|
|
result = []
|
|
_append_range_or_elements(result, 1, 5)
|
|
assert result == [[1, 5]]
|
|
|
|
|
|
class TestDecompressSlotMapping:
|
|
def test_empty(self):
|
|
assert decompress_slot_mapping([]) == []
|
|
|
|
def test_single_int(self):
|
|
assert decompress_slot_mapping([5]) == [5]
|
|
|
|
def test_single_range(self):
|
|
assert decompress_slot_mapping([[1, 5]]) == [1, 2, 3, 4, 5]
|
|
|
|
def test_mixed(self):
|
|
assert decompress_slot_mapping([[1, 5], 7, 8]) == [1, 2, 3, 4, 5, 7, 8]
|
|
|
|
def test_roundtrip(self):
|
|
"""Compress then decompress should return original."""
|
|
original = [1, 2, 3, 4, 5, 9, 10, 11, 12]
|
|
compressed = compress_slot_mapping(original)
|
|
assert decompress_slot_mapping(compressed) == original
|
|
|
|
|
|
class TestParseMixedSlotMapping:
|
|
def test_simple_numbers(self):
|
|
slots, err = parse_mixed_slot_mapping("1,2,3,17,19")
|
|
assert err is None
|
|
assert slots == [1, 2, 3, 17, 19]
|
|
|
|
def test_range_format(self):
|
|
slots, err = parse_mixed_slot_mapping("[9,12]")
|
|
assert err is None
|
|
assert slots == [9, 10, 11, 12]
|
|
|
|
def test_mixed_format(self):
|
|
slots, err = parse_mixed_slot_mapping("1,2,3,[9,12],17,19")
|
|
assert err is None
|
|
assert slots == [1, 2, 3, 9, 10, 11, 12, 17, 19]
|
|
|
|
def test_whitespace_handling(self):
|
|
slots, err = parse_mixed_slot_mapping(" 1 , 2 , [3 , 5] ")
|
|
assert err is None
|
|
assert slots == [1, 2, 3, 4, 5]
|
|
|
|
def test_nested_brackets_error(self):
|
|
slots, err = parse_mixed_slot_mapping("[[1,2]]")
|
|
assert slots is None
|
|
assert err is not None
|
|
assert "error" in err
|
|
|
|
def test_unmatched_closing_bracket(self):
|
|
slots, err = parse_mixed_slot_mapping("1,2]")
|
|
assert slots is None
|
|
assert err is not None
|
|
|
|
def test_unclosed_bracket(self):
|
|
slots, err = parse_mixed_slot_mapping("[1,2")
|
|
assert slots is None
|
|
assert err is not None
|
|
|
|
def test_invalid_range_start_gt_end(self):
|
|
slots, err = parse_mixed_slot_mapping("[5,2]")
|
|
assert slots is None
|
|
assert err is not None
|
|
|
|
def test_invalid_format(self):
|
|
slots, err = parse_mixed_slot_mapping("abc")
|
|
assert slots is None
|
|
assert err is not None
|
|
|
|
def test_empty_string(self):
|
|
slots, err = parse_mixed_slot_mapping("")
|
|
assert err is None
|
|
assert slots == []
|
|
|
|
|
|
# ============================================================
|
|
# Version
|
|
# ============================================================
|
|
class TestGetVersion:
|
|
def test_version_with_values(self):
|
|
with (
|
|
patch("lmcache.utils.VERSION", "1.0.0"),
|
|
patch("lmcache.utils.COMMIT_ID", "abc123"),
|
|
):
|
|
assert get_version() == "1.0.0-abc123"
|
|
|
|
def test_version_empty(self):
|
|
with patch("lmcache.utils.VERSION", ""), patch("lmcache.utils.COMMIT_ID", ""):
|
|
assert get_version() == "NA-NA"
|
|
|
|
def test_version_partial(self):
|
|
with (
|
|
patch("lmcache.utils.VERSION", "2.0"),
|
|
patch("lmcache.utils.COMMIT_ID", ""),
|
|
):
|
|
assert get_version() == "2.0-NA"
|
|
|
|
|
|
# ============================================================
|
|
# convert_tokens_to_list
|
|
# ============================================================
|
|
class TestConvertTokensToList:
|
|
def test_none_input(self):
|
|
assert convert_tokens_to_list(None, 0, 5) == []
|
|
|
|
def test_tensor_input(self):
|
|
t = torch.tensor([10, 20, 30, 40, 50])
|
|
result = convert_tokens_to_list(t, 1, 3)
|
|
assert result == [20, 30, 40]
|
|
|
|
def test_list_input(self):
|
|
tokens = [10, 20, 30, 40, 50]
|
|
result = convert_tokens_to_list(tokens, 0, 2)
|
|
assert result == [10, 20, 30]
|
|
|
|
def test_full_range(self):
|
|
t = torch.tensor([1, 2, 3])
|
|
result = convert_tokens_to_list(t, 0, 2)
|
|
assert result == [1, 2, 3]
|
|
|
|
|
|
# ============================================================
|
|
# CacheEngineKey
|
|
# ============================================================
|
|
class TestCacheEngineKey:
|
|
@pytest.fixture()
|
|
def basic_key(self):
|
|
return CacheEngineKey(
|
|
model_name="test_model",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xABCD,
|
|
dtype=torch.float16,
|
|
)
|
|
|
|
@pytest.fixture()
|
|
def key_with_tags(self):
|
|
return CacheEngineKey(
|
|
model_name="model_a",
|
|
world_size=2,
|
|
worker_id=1,
|
|
chunk_hash=0xFF,
|
|
dtype=torch.bfloat16,
|
|
request_configs={
|
|
"lmcache.tag.lora": "adapter1",
|
|
"lmcache.tag.version": "v2",
|
|
},
|
|
)
|
|
|
|
def test_hash_and_eq(self, basic_key):
|
|
key2 = CacheEngineKey(
|
|
model_name="test_model",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xABCD,
|
|
dtype=torch.float16,
|
|
)
|
|
assert basic_key == key2
|
|
assert hash(basic_key) == hash(key2)
|
|
|
|
def test_not_equal_different_hash(self, basic_key):
|
|
key2 = CacheEngineKey(
|
|
model_name="test_model",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0x1234,
|
|
dtype=torch.float16,
|
|
)
|
|
assert basic_key != key2
|
|
|
|
def test_not_equal_different_type(self, basic_key):
|
|
assert basic_key != "not_a_key"
|
|
|
|
def test_to_string_basic(self, basic_key):
|
|
s = basic_key.to_string()
|
|
assert "test_model" in s
|
|
assert "abcd" in s
|
|
assert "half" in s
|
|
|
|
def test_to_string_with_tags(self, key_with_tags):
|
|
s = key_with_tags.to_string()
|
|
assert "lora%adapter1" in s
|
|
assert "version%v2" in s
|
|
|
|
def test_from_string_roundtrip(self, basic_key):
|
|
s = basic_key.to_string()
|
|
restored = CacheEngineKey.from_string(s)
|
|
assert restored == basic_key
|
|
|
|
def test_from_string_with_tags_roundtrip(self, key_with_tags):
|
|
s = key_with_tags.to_string()
|
|
restored = CacheEngineKey.from_string(s)
|
|
assert restored == key_with_tags
|
|
|
|
def test_from_string_invalid(self):
|
|
with pytest.raises(ValueError):
|
|
CacheEngineKey.from_string("invalid")
|
|
|
|
def test_from_string_invalid_tag_format(self):
|
|
with pytest.raises(ValueError):
|
|
CacheEngineKey.from_string("model@1@0@abcd@half@badtag")
|
|
|
|
def test_to_dict_and_from_dict(self, basic_key):
|
|
d = basic_key.to_dict()
|
|
assert d["__type__"] == "CacheEngineKey"
|
|
restored = CacheEngineKey.from_dict(d)
|
|
assert restored == basic_key
|
|
|
|
def test_to_dict_with_tags(self, key_with_tags):
|
|
d = key_with_tags.to_dict()
|
|
assert "request_configs" in d
|
|
|
|
def test_from_dict_invalid_tag(self):
|
|
d = {
|
|
"model_name": "m",
|
|
"world_size": 1,
|
|
"worker_id": 0,
|
|
"chunk_hash": 0xAB,
|
|
"dtype": "half",
|
|
"request_configs": ["badformat"],
|
|
}
|
|
with pytest.raises(ValueError):
|
|
CacheEngineKey.from_dict(d)
|
|
|
|
def test_with_new_worker_id(self, basic_key):
|
|
new_key = basic_key.with_new_worker_id(42)
|
|
assert new_key.worker_id == 42
|
|
assert new_key.model_name == basic_key.model_name
|
|
assert new_key.chunk_hash == basic_key.chunk_hash
|
|
|
|
def test_split_layers(self, basic_key):
|
|
layers = basic_key.split_layers(3)
|
|
assert len(layers) == 3
|
|
for i, layer_key in enumerate(layers):
|
|
assert isinstance(layer_key, LayerCacheEngineKey)
|
|
assert layer_key.layer_id == i
|
|
assert layer_key.model_name == "test_model"
|
|
|
|
def test_get_first_layer(self, basic_key):
|
|
first = basic_key.get_first_layer()
|
|
assert isinstance(first, LayerCacheEngineKey)
|
|
assert first.layer_id == 0
|
|
assert first.model_name == basic_key.model_name
|
|
|
|
def test_chunk_hash_hex(self, basic_key):
|
|
assert basic_key.chunk_hash_hex == "abcd"
|
|
|
|
def test_chunk_hash_hex_bytes(self):
|
|
key = CacheEngineKey(
|
|
model_name="m",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xFF,
|
|
dtype=torch.float16,
|
|
)
|
|
# Manually set chunk_hash to bytes for coverage
|
|
object.__setattr__(key, "chunk_hash", b"\xab\xcd")
|
|
assert key.chunk_hash_hex == "abcd"
|
|
|
|
def test_unsupported_dtype_raises(self):
|
|
with pytest.raises(ValueError, match="Unsupported"):
|
|
CacheEngineKey(
|
|
model_name="m",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0x1,
|
|
dtype=torch.complex64,
|
|
)
|
|
|
|
def test_no_tags_when_no_tag_prefix(self):
|
|
key = CacheEngineKey(
|
|
model_name="m",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0x1,
|
|
dtype=torch.float16,
|
|
request_configs={"some_config": "value"},
|
|
)
|
|
assert key.tags is None
|
|
|
|
|
|
# ============================================================
|
|
# LayerCacheEngineKey
|
|
# ============================================================
|
|
class TestLayerCacheEngineKey:
|
|
@pytest.fixture()
|
|
def layer_key(self):
|
|
return LayerCacheEngineKey(
|
|
model_name="model_b",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xBEEF,
|
|
dtype=torch.float16,
|
|
layer_id=5,
|
|
)
|
|
|
|
def test_hash_includes_layer_id(self, layer_key):
|
|
other = LayerCacheEngineKey(
|
|
model_name="model_b",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xBEEF,
|
|
dtype=torch.float16,
|
|
layer_id=6,
|
|
)
|
|
assert layer_key != other
|
|
assert hash(layer_key) != hash(other)
|
|
|
|
def test_eq_same_layer(self, layer_key):
|
|
same = LayerCacheEngineKey(
|
|
model_name="model_b",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xBEEF,
|
|
dtype=torch.float16,
|
|
layer_id=5,
|
|
)
|
|
assert layer_key == same
|
|
|
|
def test_eq_different_base_key(self, layer_key):
|
|
different = LayerCacheEngineKey(
|
|
model_name="other_model",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xBEEF,
|
|
dtype=torch.float16,
|
|
layer_id=5,
|
|
)
|
|
assert layer_key != different
|
|
|
|
def test_to_string(self, layer_key):
|
|
s = layer_key.to_string()
|
|
assert "model_b" in s
|
|
assert "beef" in s
|
|
assert "@5" in s
|
|
|
|
def test_to_string_with_tags(self):
|
|
key = LayerCacheEngineKey(
|
|
model_name="m",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xAB,
|
|
dtype=torch.float16,
|
|
layer_id=2,
|
|
request_configs={"lmcache.tag.foo": "bar"},
|
|
)
|
|
s = key.to_string()
|
|
assert "foo%bar" in s
|
|
|
|
def test_from_string_roundtrip(self, layer_key):
|
|
s = layer_key.to_string()
|
|
restored = LayerCacheEngineKey.from_string(s)
|
|
assert restored == layer_key
|
|
|
|
def test_from_string_with_tags(self):
|
|
key = LayerCacheEngineKey(
|
|
model_name="m",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xAB,
|
|
dtype=torch.float16,
|
|
layer_id=3,
|
|
request_configs={"lmcache.tag.t1": "v1"},
|
|
)
|
|
s = key.to_string()
|
|
restored = LayerCacheEngineKey.from_string(s)
|
|
assert restored == key
|
|
|
|
def test_from_string_invalid(self):
|
|
with pytest.raises(ValueError):
|
|
LayerCacheEngineKey.from_string("a@b")
|
|
|
|
def test_from_string_invalid_tag(self):
|
|
with pytest.raises(ValueError):
|
|
LayerCacheEngineKey.from_string("m@1@0@ab@half@3@badtag")
|
|
|
|
def test_split_layers(self, layer_key):
|
|
layers = layer_key.split_layers(4)
|
|
assert len(layers) == 4
|
|
for i, lk in enumerate(layers):
|
|
assert lk.layer_id == i
|
|
|
|
|
|
# ============================================================
|
|
# parse_cache_key
|
|
# ============================================================
|
|
class TestCacheKeyParsing:
|
|
def test_parse_cache_engine_key(self):
|
|
key = CacheEngineKey(
|
|
model_name="m",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xABC,
|
|
dtype=torch.float16,
|
|
)
|
|
s = key.to_string()
|
|
parsed = parse_cache_key(s)
|
|
assert isinstance(parsed, CacheEngineKey)
|
|
assert parsed == key
|
|
|
|
@pytest.mark.xfail(
|
|
reason="parse_cache_key bug: cannot parse LayerCacheEngineKey with dtype"
|
|
)
|
|
def test_parse_layer_cache_engine_key(self):
|
|
key = LayerCacheEngineKey(
|
|
model_name="m",
|
|
world_size=1,
|
|
worker_id=0,
|
|
chunk_hash=0xABC,
|
|
dtype=torch.float16,
|
|
layer_id=7,
|
|
)
|
|
s = key.to_string()
|
|
parsed = parse_cache_key(s)
|
|
assert isinstance(parsed, LayerCacheEngineKey)
|
|
assert parsed == key
|
|
|
|
"""
|
|
Tests for CacheEngineKey and LayerCacheEngineKey parsing.
|
|
"""
|
|
|
|
def test_parse_cache_key_without_tags(self):
|
|
"""
|
|
Test parsing CacheEngineKey without tags.
|
|
"""
|
|
key_str = "model@2@0@abc123@bfloat16"
|
|
result = parse_cache_key(key_str)
|
|
|
|
assert isinstance(result, CacheEngineKey)
|
|
assert not isinstance(result, LayerCacheEngineKey)
|
|
assert result.model_name == "model"
|
|
assert result.world_size == 2
|
|
assert result.worker_id == 0
|
|
assert result.chunk_hash == 0xABC123
|
|
assert result.dtype == torch.bfloat16
|
|
|
|
def test_parse_cache_key_with_tags(self):
|
|
"""
|
|
Test parsing CacheEngineKey with tags.
|
|
"""
|
|
key_str = "model@2@0@abc123@bfloat16@mytag%myvalue@othertag%othervalue"
|
|
result = parse_cache_key(key_str)
|
|
|
|
# This should be CacheEngineKey, not LayerCacheEngineKey
|
|
assert isinstance(result, CacheEngineKey)
|
|
assert not isinstance(result, LayerCacheEngineKey)
|
|
assert result.model_name == "model"
|
|
assert result.world_size == 2
|
|
assert result.worker_id == 0
|
|
assert result.chunk_hash == 0xABC123
|
|
assert result.dtype == torch.bfloat16
|
|
assert result.tags is not None
|
|
assert ("mytag", "myvalue") in result.tags
|
|
assert ("othertag", "othervalue") in result.tags
|
|
|
|
def test_parse_layer_cache_key_without_tags(self):
|
|
"""
|
|
Test parsing LayerCacheEngineKey without tags.
|
|
"""
|
|
key_str = "model@2@0@abc123@bfloat16@5"
|
|
result = parse_cache_key(key_str)
|
|
|
|
assert isinstance(result, LayerCacheEngineKey)
|
|
assert result.model_name == "model"
|
|
assert result.world_size == 2
|
|
assert result.worker_id == 0
|
|
assert result.chunk_hash == 0xABC123
|
|
assert result.dtype == torch.bfloat16
|
|
assert result.layer_id == 5
|
|
|
|
def test_parse_layer_cache_key_with_tags(self):
|
|
"""
|
|
Test parsing LayerCacheEngineKey with tags - demonstrates the bug.
|
|
"""
|
|
key_str = "model@2@0@abc123@bfloat16@5@mytag%myvalue"
|
|
result = parse_cache_key(key_str)
|
|
|
|
# This should be LayerCacheEngineKey with layer_id=5 and tags
|
|
assert isinstance(result, LayerCacheEngineKey)
|
|
assert result.model_name == "model"
|
|
assert result.world_size == 2
|
|
assert result.worker_id == 0
|
|
assert result.chunk_hash == 0xABC123
|
|
assert result.dtype == torch.bfloat16
|
|
assert result.layer_id == 5
|
|
assert result.tags is not None
|
|
assert ("mytag", "myvalue") in result.tags
|
|
|
|
def test_roundtrip_cache_engine_key_without_tags(self):
|
|
"""
|
|
Test that CacheEngineKey serialization round-trips correctly.
|
|
"""
|
|
original = CacheEngineKey(
|
|
model_name="test-model",
|
|
world_size=4,
|
|
worker_id=1,
|
|
chunk_hash=0xDEADBEEF,
|
|
dtype=torch.float16,
|
|
request_configs=None,
|
|
)
|
|
|
|
key_str = original.to_string()
|
|
parsed = parse_cache_key(key_str)
|
|
|
|
assert isinstance(parsed, CacheEngineKey)
|
|
assert not isinstance(parsed, LayerCacheEngineKey)
|
|
assert parsed == original
|
|
|
|
def test_roundtrip_cache_engine_key_with_tags(self):
|
|
"""
|
|
Test that CacheEngineKey with tags serialization round-trips correctly.
|
|
"""
|
|
original = CacheEngineKey(
|
|
model_name="test-model",
|
|
world_size=4,
|
|
worker_id=1,
|
|
chunk_hash=0xDEADBEEF,
|
|
dtype=torch.float32,
|
|
request_configs={
|
|
"lmcache.tag.user": "alice",
|
|
"lmcache.tag.session": "xyz789",
|
|
},
|
|
)
|
|
|
|
key_str = original.to_string()
|
|
parsed = parse_cache_key(key_str)
|
|
|
|
assert isinstance(parsed, CacheEngineKey)
|
|
assert not isinstance(parsed, LayerCacheEngineKey)
|
|
assert parsed == original
|
|
|
|
def test_roundtrip_layer_cache_engine_key_without_tags(self):
|
|
"""
|
|
Test that LayerCacheEngineKey serialization round-trips correctly.
|
|
"""
|
|
original = LayerCacheEngineKey(
|
|
model_name="test-model",
|
|
world_size=4,
|
|
worker_id=1,
|
|
chunk_hash=0xDEADBEEF,
|
|
dtype=torch.bfloat16,
|
|
request_configs=None,
|
|
layer_id=7,
|
|
)
|
|
|
|
key_str = original.to_string()
|
|
parsed = parse_cache_key(key_str)
|
|
|
|
assert isinstance(parsed, LayerCacheEngineKey)
|
|
assert parsed == original
|
|
assert parsed.layer_id == 7
|
|
|
|
def test_roundtrip_layer_cache_engine_key_with_tags(self):
|
|
"""
|
|
Test that LayerCacheEngineKey with tags serialization round-trips correctly.
|
|
"""
|
|
original = LayerCacheEngineKey(
|
|
model_name="test-model",
|
|
world_size=4,
|
|
worker_id=1,
|
|
chunk_hash=0xDEADBEEF,
|
|
dtype=torch.bfloat16,
|
|
request_configs={
|
|
"lmcache.tag.user": "bob",
|
|
"lmcache.tag.priority": "high",
|
|
},
|
|
layer_id=3,
|
|
)
|
|
|
|
key_str = original.to_string()
|
|
parsed = parse_cache_key(key_str)
|
|
|
|
assert isinstance(parsed, LayerCacheEngineKey)
|
|
assert parsed == original
|
|
assert parsed.layer_id == 3
|