158 lines
5.5 KiB
Python
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
|