Files
lmcache--lmcache/tests/v1/test_token_database.py
2026-07-13 12:24:33 +08:00

158 lines
5.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
import hashlib
import os
# Third Party
import pytest
import torch
# First Party
from lmcache.v1.config import LMCacheEngineConfig
from lmcache.v1.token_database import ChunkedTokenDatabase, SegmentTokenDatabase
# Local
from .utils import dumb_metadata, dumb_metadata_with_model_name, generate_tokens
def hf_credentials_available() -> bool:
token_env = os.getenv("HF_TOKEN")
hf_home = os.getenv("HF_HOME")
default_token_file = os.path.expanduser("~/.cache/huggingface/token")
token_file = os.path.join(hf_home, "token") if hf_home else ""
return bool(
token_env or os.path.exists(default_token_file) or os.path.exists(token_file)
)
@pytest.mark.parametrize("chunk_length", [16, 64, 256])
@pytest.mark.parametrize("save_unfull_chunk", [False, True])
def test_chunked_token_database(chunk_length, save_unfull_chunk):
cfg = LMCacheEngineConfig.from_legacy(
chunk_size=chunk_length, backend="cpu", save_unfull_chunk=save_unfull_chunk
)
metadata = dumb_metadata()
test_length = 2500
tokens = generate_tokens(test_length, "cpu")
mask = torch.full([test_length], True, dtype=torch.bool, device="cpu")
num_falses = [i * chunk_length for i in range(0, test_length // chunk_length)]
db = ChunkedTokenDatabase(cfg, metadata)
# Process without mask
original_results = list(db.process_tokens(tokens=tokens))
end = (
test_length if save_unfull_chunk else (test_length - test_length % chunk_length)
)
for i in range(0, end, chunk_length):
st, ed, key = original_results[i // chunk_length]
assert st == i
if save_unfull_chunk:
assert ed == min(i + chunk_length, test_length)
else:
assert ed == i + chunk_length
for i in range(0, test_length // chunk_length):
mask[: num_falses[i]] = False
new_results = list(db.process_tokens(tokens=tokens, mask=mask))
assert len(new_results) == len(original_results) - i
for j in range(len(new_results)):
st, ed, key = new_results[j]
assert st == original_results[j + i][0]
assert ed == original_results[j + i][1]
@pytest.mark.parametrize("prefix_length", [0, 16, 64, 256])
@pytest.mark.parametrize("chunk_lengths", [[256, 512, 256], [1024, 512, 256]])
@pytest.mark.skipif(
not hf_credentials_available(), reason="No Hugging Face credentials found"
)
def test_segment_token_database(prefix_length, chunk_lengths):
cfg = LMCacheEngineConfig.from_legacy(blend_special_str=" # # ")
metadata = dumb_metadata_with_model_name("facebook/opt-125m")
db = SegmentTokenDatabase(cfg, metadata)
sep_tokens = db.sep_tokens
sys_length = 25
query_length = 50
sys_tokens = generate_tokens(sys_length, "cpu", fixed=True)
query_tokens = generate_tokens(query_length, "cpu", fixed=True)
token_chunks = []
starts = [0]
ends = [sys_length]
sys_tuple = tuple(sys_tokens.cpu().tolist())
sys_hash = hash((None, sys_tuple, None))
hashes = [sys_hash]
start = sys_length + len(sep_tokens)
for idx, chunk_length in enumerate(chunk_lengths):
token_chunk = generate_tokens(chunk_length, "cpu", fixed=True)
token_tuple = tuple(token_chunk.cpu().tolist())
token_hash = hash((None, token_tuple, None))
hashes.append(token_hash)
token_chunk = torch.cat([sep_tokens, token_chunk])
token_chunks.append(token_chunk)
starts.append(start)
ends.append(start + chunk_length)
start += chunk_length + len(sep_tokens)
query_tuple = tuple(query_tokens.cpu().tolist())
query_hash = hash((None, query_tuple, None))
hashes.append(query_hash)
starts.append(start)
ends.append(start + query_length)
tokens = torch.cat([sys_tokens, *token_chunks, sep_tokens, query_tokens])
total_length = len(tokens)
mask = torch.full([total_length], True, dtype=torch.bool, device="cpu")
mask[:prefix_length] = False
chunk_lists = [sys_tokens, *token_chunks, sep_tokens, query_tokens]
skip_chunk_num = 0
cum_length = 0
for chunk in chunk_lists:
if prefix_length > cum_length:
skip_chunk_num += 1
cum_length += len(chunk)
starts = starts[skip_chunk_num:]
ends = ends[skip_chunk_num:]
hashes = hashes[skip_chunk_num:]
original_results = list(db.process_tokens(tokens=tokens, mask=mask))
for i in range(len(original_results)):
st, ed, key = original_results[i]
assert st == starts[i]
assert ed == ends[i]
assert key.chunk_hash == hashes[i]
# print(st, starts[i])
# print(ed, ends[i])
def test_process_tokens_returns_int_keys_for_bytes_hash_func() -> None:
"""process_tokens must produce int chunk_hash keys even when the underlying
hash function returns bytes (e.g. sha256_cbor). This ensures downstream
code such as msgpack serialisation always receives plain ints.
"""
cfg = LMCacheEngineConfig.from_legacy(chunk_size=16, backend="cpu")
metadata = dumb_metadata()
db = ChunkedTokenDatabase(cfg, metadata)
# Stub that returns bytes, mimicking sha256_cbor without requiring cbor2.
db.hash_func = lambda x: hashlib.sha256(str(x).encode()).digest()
tokens = generate_tokens(32, "cpu")
results = list(db.process_tokens(tokens=tokens, make_key=False))
assert len(results) > 0
for _, _, hash_val in results:
assert isinstance(hash_val, int), f"Expected int, got {type(hash_val)}"
# Must fit in uint64 (msgpack range: 0 to 2**64 - 1)
assert 0 <= hash_val <= 2**64 - 1