+11








f50c5a413f
Signed-off-by: JuanPZuluaga <juanz9312@gmal.com> Signed-off-by: yiliu30 <yi4.liu@intel.com> Signed-off-by: gcanlin <canlinguosdu@gmail.com> Signed-off-by: Binh Tang <tangbinhna@gmail.com> Signed-off-by: Binh Tang <binht@netflix.com> Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com> Signed-off-by: Rein Yang <ruiruyang2@gmail.com> Signed-off-by: CHEN <116010019@link.cuhk.edu.cn> Signed-off-by: vraiti <vraiti@redhat.com> Signed-off-by: Songrui625 <songrui625@gmail.com> Signed-off-by: Lidang Jiang <lidangjiang@gmail.com> Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com> Signed-off-by: Alex Brooks <albrooks@redhat.com> Co-authored-by: JuanPZuluaga <juanz9312@gmal.com> Co-authored-by: Yi Liu <yi4.liu@intel.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com> Co-authored-by: Canlin Guo <canlinguosdu@gmail.com> Co-authored-by: Binh Tang <tangbinhna@gmail.com> Co-authored-by: Binh Tang <binht@netflix.com> Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com> Co-authored-by: rein yang <73573651+R2-Y@users.noreply.github.com> Co-authored-by: zhumingjue138 <zhumingjue@huawei.com> Co-authored-by: ChenWenjing <54166744+Shirley125@users.noreply.github.com> Co-authored-by: vraiti <vraiti@redhat.com> Co-authored-by: 汪志鹏 <wangzhipeng628@gmail.com> Co-authored-by: Sy03 <1370724210@qq.com> Co-authored-by: chickeyton <ngton2014@gmail.com> Co-authored-by: Jerry Song <46962917+Songrui625@users.noreply.github.com> Co-authored-by: Lidang Jiang <119769478+Lidang-Jiang@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Alex Brooks <albrooks@redhat.com> Co-authored-by: linyueqian <linyueqian@outlook.com> Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
130 lines
4.9 KiB
Python
130 lines
4.9 KiB
Python
import threading
|
|
|
|
import pytest
|
|
|
|
from vllm_omni.utils.voice_cache import VoiceEmbeddingCache
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
|
|
|
|
@pytest.fixture
|
|
def cache():
|
|
return VoiceEmbeddingCache(max_entries=4)
|
|
|
|
|
|
class TestVoiceEmbeddingCache:
|
|
def test_miss_returns_none(self, cache: VoiceEmbeddingCache):
|
|
assert cache.get("nonexistent") is None
|
|
assert cache.stats()["misses"] == 1
|
|
|
|
def test_put_and_hit(self, cache: VoiceEmbeddingCache):
|
|
cache.put("abc", {"val": 42})
|
|
result = cache.get("abc")
|
|
assert result is not None
|
|
assert result["val"] == 42
|
|
assert cache.stats()["hits"] == 1
|
|
|
|
def test_lru_eviction(self, cache: VoiceEmbeddingCache):
|
|
for i in range(5):
|
|
cache.put(f"key{i}", {"i": i})
|
|
# key0 should have been evicted (oldest, max_entries=4)
|
|
assert cache.get("key0") is None
|
|
# key1..key4 should still be present
|
|
for i in range(1, 5):
|
|
assert cache.get(f"key{i}") is not None
|
|
assert cache.stats()["entries"] == 4
|
|
|
|
def test_lru_access_promotes(self, cache: VoiceEmbeddingCache):
|
|
cache.put("a", {"v": 1})
|
|
cache.put("b", {"v": 2})
|
|
cache.put("c", {"v": 3})
|
|
cache.put("d", {"v": 4})
|
|
# Access "a" to promote it to MRU
|
|
cache.get("a")
|
|
# Insert "e" -- should evict "b" (now the oldest), not "a"
|
|
cache.put("e", {"v": 5})
|
|
assert cache.get("a") is not None
|
|
assert cache.get("b") is None
|
|
|
|
def test_put_overwrites(self, cache: VoiceEmbeddingCache):
|
|
cache.put("k", {"old": True})
|
|
cache.put("k", {"new": True})
|
|
result = cache.get("k")
|
|
assert result is not None
|
|
assert "new" in result
|
|
assert "old" not in result
|
|
assert cache.stats()["entries"] == 1
|
|
|
|
def test_make_cache_key_includes_mode(self):
|
|
k1 = VoiceEmbeddingCache.make_cache_key("alice", xvec_only=True)
|
|
k2 = VoiceEmbeddingCache.make_cache_key("alice", xvec_only=False)
|
|
assert k1 != k2
|
|
assert "xvec" in k1
|
|
assert "icl" in k2
|
|
|
|
def test_make_cache_key_deterministic(self):
|
|
k1 = VoiceEmbeddingCache.make_cache_key("bob", xvec_only=True)
|
|
k2 = VoiceEmbeddingCache.make_cache_key("bob", xvec_only=True)
|
|
assert k1 == k2
|
|
|
|
def test_make_cache_key_created_at_isolation(self):
|
|
"""Different created_at timestamps must produce different keys (stale-cache protection)."""
|
|
k1 = VoiceEmbeddingCache.make_cache_key("alice", xvec_only=False, created_at=1000.0)
|
|
k2 = VoiceEmbeddingCache.make_cache_key("alice", xvec_only=False, created_at=2000.0)
|
|
assert k1 != k2
|
|
|
|
def test_stale_cache_protection(self, cache: VoiceEmbeddingCache):
|
|
"""Re-upload (new created_at) must produce a cache miss, not a stale hit."""
|
|
key_old = VoiceEmbeddingCache.make_cache_key("alice", xvec_only=False, created_at=1000.0)
|
|
key_new = VoiceEmbeddingCache.make_cache_key("alice", xvec_only=False, created_at=2000.0)
|
|
cache.put(key_old, {"ref_spk_embedding": "old_emb"})
|
|
# Re-upload produces a new created_at → different key → cold miss
|
|
assert cache.get(key_new) is None
|
|
# Old key still in cache (not yet evicted)
|
|
assert cache.get(key_old) is not None
|
|
|
|
def test_cache_mode_isolation(self, cache: VoiceEmbeddingCache):
|
|
"""xvec entry must NOT be served for an icl request (same voice)."""
|
|
key_xvec = VoiceEmbeddingCache.make_cache_key("alice", xvec_only=True)
|
|
key_icl = VoiceEmbeddingCache.make_cache_key("alice", xvec_only=False)
|
|
cache.put(key_xvec, {"ref_code": None, "ref_spk_embedding": "emb"})
|
|
# icl request should miss — different key
|
|
assert cache.get(key_icl) is None
|
|
# xvec request should hit
|
|
assert cache.get(key_xvec) is not None
|
|
|
|
def test_stats_counters(self, cache: VoiceEmbeddingCache):
|
|
cache.put("x", {"v": 1})
|
|
cache.get("x") # hit
|
|
cache.get("x") # hit
|
|
cache.get("y") # miss
|
|
s = cache.stats()
|
|
assert s["hits"] == 2
|
|
assert s["misses"] == 1
|
|
assert s["entries"] == 1
|
|
assert s["max_entries"] == 4
|
|
|
|
def test_thread_safety(self):
|
|
cache = VoiceEmbeddingCache(max_entries=32)
|
|
errors = []
|
|
|
|
def worker(thread_id: int):
|
|
try:
|
|
for i in range(50):
|
|
key = f"t{thread_id}_k{i}"
|
|
cache.put(key, {"tid": thread_id, "i": i})
|
|
cache.get(key)
|
|
cache.get(f"t{(thread_id + 1) % 10}_k{i}")
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
threads = [threading.Thread(target=worker, args=(t,)) for t in range(10)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
assert not errors, f"Thread safety errors: {errors}"
|
|
s = cache.stats()
|
|
assert s["entries"] <= 32
|