Files
2026-07-13 12:24:33 +08:00

1077 lines
36 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for BlendTracingSubscriber."""
# Standard
from unittest.mock import patch
import time
# Third Party
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
import pytest
# First Party
from lmcache.v1.mp_observability.event import Event, EventType
from lmcache.v1.mp_observability.event_bus import EventBus, EventBusConfig
from lmcache.v1.mp_observability.subscribers.tracing.cb_server import (
BlendTracingSubscriber,
)
from lmcache.v1.mp_observability.subscribers.tracing.span_registry import (
SpanRegistry,
)
import lmcache.v1.mp_observability.subscribers.tracing.cb_server as cb_server_module
@pytest.fixture
def bus():
return EventBus(EventBusConfig(enabled=True, max_queue_size=100))
@pytest.fixture
def registry():
return SpanRegistry()
@pytest.fixture
def subscriber(registry, bus):
sub = BlendTracingSubscriber(registry)
bus.register_subscriber(sub)
return sub
class TestBlendTracingSubscriber:
def test_subscriptions_cover_all_cb_events(self, subscriber):
subs = subscriber.get_subscriptions()
assert EventType.CB_REQUEST_START in subs
assert EventType.CB_REQUEST_END in subs
assert EventType.CB_STORE_PRE_COMPUTED_SUBMITTED in subs
assert EventType.CB_RETRIEVE_SUBMITTED in subs
assert EventType.CB_STORE_FINAL_SUBMITTED in subs
assert EventType.CB_LOOKUP_START in subs
assert EventType.CB_LOOKUP_END in subs
assert EventType.CB_STORE_PRE_COMPUTED_START in subs
assert EventType.CB_STORE_PRE_COMPUTED_END in subs
assert EventType.CB_RETRIEVE_START in subs
assert EventType.CB_RETRIEVE_END in subs
assert EventType.CB_STORE_FINAL_START in subs
assert EventType.CB_STORE_FINAL_END in subs
assert EventType.CB_FINGERPRINTS_REGISTERED in subs
assert EventType.CB_CHUNKS_EVICTED in subs
# ------------------------------------------------------------------
# Root span creation
# ------------------------------------------------------------------
def test_root_span_created_on_request_start(self, bus, registry, subscriber):
bus.start()
bus.publish(Event(event_type=EventType.CB_REQUEST_START, session_id="req-root"))
time.sleep(0.15)
assert registry.get("req-root", "cb.request") is not None
bus.stop()
def test_no_root_span_before_any_event(self, registry):
assert registry.get("any-session", "cb.request") is None
# ------------------------------------------------------------------
# Session end closes root immediately when no GPU ops in flight
# ------------------------------------------------------------------
def test_session_end_closes_root_immediately_when_no_gpu_ops(
self, bus, registry, subscriber
):
bus.start()
now = time.time()
sid = "req-lookup-only"
bus.publish(
Event(
event_type=EventType.CB_REQUEST_START,
session_id=sid,
timestamp=now,
)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id=sid,
timestamp=now + 0.001,
metadata={"num_tokens": 64},
)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id=sid,
timestamp=now + 0.010,
metadata={
"num_tokens": 64,
"fingerprint_hits": 2,
"storage_hits": 2,
"stale_chunks": 0,
"no_gpu_context": False,
},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.020,
)
)
time.sleep(0.15)
bus.stop()
assert registry.get(sid, "cb.request") is None
assert sid not in subscriber._pending_gpu_ops
assert len(subscriber._pending) == 0
# ------------------------------------------------------------------
# Deferred close: SESSION_END races GPU store_pre_computed
# ------------------------------------------------------------------
def test_session_end_deferred_until_store_pre_computed_finishes(
self, bus, registry, subscriber
):
bus.start()
now = time.time()
sid = "req-deferred-store-pre"
bus.publish(
Event(
event_type=EventType.CB_REQUEST_START,
session_id=sid,
timestamp=now,
)
)
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_SUBMITTED,
session_id=sid,
timestamp=now + 0.001,
metadata={"instance_id": 0},
)
)
# SESSION_END arrives before GPU store completes
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.005,
)
)
time.sleep(0.15)
# Root should still be open
assert registry.get(sid, "cb.request") is not None
assert sid in subscriber._deferred_session_end_ts
# GPU store completes
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_START,
session_id=sid,
timestamp=now + 0.010,
metadata={"instance_id": 0, "num_tokens": 64},
)
)
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_END,
session_id=sid,
timestamp=now + 0.050,
metadata={"instance_id": 0, "stored_chunks": 4, "success": True},
)
)
time.sleep(0.15)
bus.stop()
assert registry.get(sid, "cb.request") is None
assert sid not in subscriber._deferred_session_end_ts
assert sid not in subscriber._pending_gpu_ops
# ------------------------------------------------------------------
# Deferred close: SESSION_END races GPU retrieve
# ------------------------------------------------------------------
def test_session_end_deferred_until_retrieve_finishes(
self, bus, registry, subscriber
):
bus.start()
now = time.time()
sid = "req-deferred-retrieve"
bus.publish(
Event(
event_type=EventType.CB_REQUEST_START,
session_id=sid,
timestamp=now,
)
)
bus.publish(
Event(
event_type=EventType.CB_RETRIEVE_SUBMITTED,
session_id=sid,
timestamp=now + 0.001,
metadata={"instance_id": 1},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.005,
)
)
time.sleep(0.15)
assert registry.get(sid, "cb.request") is not None
assert sid in subscriber._deferred_session_end_ts
bus.publish(
Event(
event_type=EventType.CB_RETRIEVE_START,
session_id=sid,
timestamp=now + 0.010,
metadata={"instance_id": 1, "num_chunks": 3},
)
)
bus.publish(
Event(
event_type=EventType.CB_RETRIEVE_END,
session_id=sid,
timestamp=now + 0.050,
metadata={"instance_id": 1, "num_chunks": 3, "success": True},
)
)
time.sleep(0.15)
bus.stop()
assert registry.get(sid, "cb.request") is None
assert sid not in subscriber._deferred_session_end_ts
assert sid not in subscriber._pending_gpu_ops
# ------------------------------------------------------------------
# Deferred close: SESSION_END races GPU store_final
# ------------------------------------------------------------------
def test_session_end_deferred_until_store_final_finishes(
self, bus, registry, subscriber
):
bus.start()
now = time.time()
sid = "req-deferred-store-final"
bus.publish(
Event(
event_type=EventType.CB_REQUEST_START,
session_id=sid,
timestamp=now,
)
)
bus.publish(
Event(
event_type=EventType.CB_STORE_FINAL_SUBMITTED,
session_id=sid,
timestamp=now + 0.001,
metadata={"instance_id": 2},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.005,
)
)
time.sleep(0.15)
assert registry.get(sid, "cb.request") is not None
assert sid in subscriber._deferred_session_end_ts
bus.publish(
Event(
event_type=EventType.CB_STORE_FINAL_START,
session_id=sid,
timestamp=now + 0.010,
metadata={"instance_id": 2, "num_tokens": 256},
)
)
bus.publish(
Event(
event_type=EventType.CB_STORE_FINAL_END,
session_id=sid,
timestamp=now + 0.060,
metadata={"instance_id": 2, "stored_chunks": 16, "success": True},
)
)
time.sleep(0.15)
bus.stop()
assert registry.get(sid, "cb.request") is None
assert sid not in subscriber._deferred_session_end_ts
assert sid not in subscriber._pending_gpu_ops
# ------------------------------------------------------------------
# Multiple GPU ops: all must finish before root closes
# ------------------------------------------------------------------
def test_multiple_gpu_ops_all_must_finish(self, bus, registry, subscriber):
bus.start()
now = time.time()
sid = "req-multi-gpu-ops"
bus.publish(
Event(
event_type=EventType.CB_REQUEST_START,
session_id=sid,
timestamp=now,
)
)
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_SUBMITTED,
session_id=sid,
timestamp=now + 0.001,
metadata={"instance_id": 0},
)
)
bus.publish(
Event(
event_type=EventType.CB_RETRIEVE_SUBMITTED,
session_id=sid,
timestamp=now + 0.002,
metadata={"instance_id": 0},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.005,
)
)
time.sleep(0.15)
# Both in flight — root still open
assert registry.get(sid, "cb.request") is not None
# Store finishes first — retrieve still pending, root stays open
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_START,
session_id=sid,
timestamp=now + 0.010,
metadata={"instance_id": 0, "num_tokens": 64},
)
)
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_END,
session_id=sid,
timestamp=now + 0.030,
metadata={"instance_id": 0, "stored_chunks": 4, "success": True},
)
)
time.sleep(0.15)
assert registry.get(sid, "cb.request") is not None
# Retrieve finishes — all done, root closes
bus.publish(
Event(
event_type=EventType.CB_RETRIEVE_START,
session_id=sid,
timestamp=now + 0.040,
metadata={"instance_id": 0, "num_chunks": 4},
)
)
bus.publish(
Event(
event_type=EventType.CB_RETRIEVE_END,
session_id=sid,
timestamp=now + 0.060,
metadata={"instance_id": 0, "num_chunks": 4, "success": True},
)
)
time.sleep(0.15)
bus.stop()
assert registry.get(sid, "cb.request") is None
assert sid not in subscriber._deferred_session_end_ts
# ------------------------------------------------------------------
# Child span lifecycles
# ------------------------------------------------------------------
def test_lookup_span_lifecycle(self, bus, subscriber):
bus.start()
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id="req-lookup",
metadata={"num_tokens": 128},
)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id="req-lookup",
metadata={
"num_tokens": 128,
"fingerprint_hits": 3,
"storage_hits": 2,
"stale_chunks": 1,
"no_gpu_context": False,
},
)
)
time.sleep(0.15)
bus.stop()
assert len(subscriber._pending) == 0
def test_store_pre_computed_span_lifecycle(self, bus, subscriber):
bus.start()
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_START,
session_id="req-sp",
metadata={"instance_id": 0, "num_tokens": 64},
)
)
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_END,
session_id="req-sp",
metadata={"instance_id": 0, "stored_chunks": 4, "success": True},
)
)
time.sleep(0.15)
bus.stop()
assert len(subscriber._pending) == 0
def test_retrieve_span_lifecycle(self, bus, subscriber):
bus.start()
bus.publish(
Event(
event_type=EventType.CB_RETRIEVE_START,
session_id="req-ret",
metadata={"instance_id": 1, "num_chunks": 3},
)
)
bus.publish(
Event(
event_type=EventType.CB_RETRIEVE_END,
session_id="req-ret",
metadata={"instance_id": 1, "num_chunks": 3, "success": True},
)
)
time.sleep(0.15)
bus.stop()
assert len(subscriber._pending) == 0
def test_store_final_span_lifecycle(self, bus, subscriber):
bus.start()
bus.publish(
Event(
event_type=EventType.CB_STORE_FINAL_START,
session_id="req-sf",
metadata={"instance_id": 2, "num_tokens": 512},
)
)
bus.publish(
Event(
event_type=EventType.CB_STORE_FINAL_END,
session_id="req-sf",
metadata={"instance_id": 2, "stored_chunks": 32, "success": True},
)
)
time.sleep(0.15)
bus.stop()
assert len(subscriber._pending) == 0
# ------------------------------------------------------------------
# Point events
# ------------------------------------------------------------------
def test_fingerprints_registered_no_crash(self, bus, subscriber):
bus.start()
bus.publish(
Event(
event_type=EventType.CB_FINGERPRINTS_REGISTERED,
session_id="req-fp",
metadata={"num_chunks": 8, "num_tokens": 256},
)
)
time.sleep(0.15)
bus.stop()
def test_chunks_evicted_no_crash(self, bus, subscriber):
bus.start()
bus.publish(
Event(
event_type=EventType.CB_CHUNKS_EVICTED,
session_id="req-ev",
metadata={"num_chunks": 2},
)
)
time.sleep(0.15)
bus.stop()
# ------------------------------------------------------------------
# Error resilience
# ------------------------------------------------------------------
def test_unmatched_end_does_not_crash(self, bus, subscriber):
bus.start()
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_END,
session_id="orphan",
metadata={"stored_chunks": 2, "success": True},
)
)
time.sleep(0.15)
bus.stop()
assert len(subscriber._pending) == 0
def test_unmatched_start_cleaned_on_shutdown(self, bus, subscriber):
bus.start()
bus.publish(
Event(
event_type=EventType.CB_STORE_PRE_COMPUTED_START,
session_id="leaked",
metadata={"instance_id": 0, "num_tokens": 64},
)
)
time.sleep(0.15)
bus.stop()
subscriber.shutdown()
assert len(subscriber._pending) == 0
def test_multiple_concurrent_sessions(self, bus, subscriber):
bus.start()
for i in range(5):
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id=f"req-{i}",
metadata={"num_tokens": 100},
)
)
for i in range(5):
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id=f"req-{i}",
metadata={
"num_tokens": 100,
"fingerprint_hits": 2,
"storage_hits": 2,
"stale_chunks": 0,
"no_gpu_context": False,
},
)
)
time.sleep(0.15)
bus.stop()
assert len(subscriber._pending) == 0
class TestCBHitRateAttributes:
"""Verify hit_tokens / requested_tokens / hit_rate are set on cb.request root span.
Uses a real OTel TracerProvider backed by InMemorySpanExporter so that span
attributes are actually recorded. The module-level ``_tracer`` is patched
for the duration of each test; ``_HAS_OTEL`` is forced True.
"""
@pytest.fixture
def exporter(self):
"""Real OTel provider with in-memory exporter; patches module tracer."""
exp = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exp))
real_tracer = provider.get_tracer("lmcache_mp.blend")
with (
patch.object(cb_server_module, "_tracer", real_tracer),
patch.object(cb_server_module, "_HAS_OTEL", True),
):
yield exp
exp.shutdown()
def _root_span(self, exporter: InMemorySpanExporter, sid: str):
"""Return the finished cb.request root span for *sid*, or None."""
for span in exporter.get_finished_spans():
if span.name == "cb.request" and span.attributes.get("session_id") == sid:
return span
return None
def test_hit_rate_attrs_set_on_root_span(self, exporter):
"""CB_LOOKUP_END with hit_tokens=512, requested_tokens=1024 → hit_rate=0.5."""
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
registry = SpanRegistry()
bus.register_subscriber(BlendTracingSubscriber(registry))
bus.start()
now = time.time()
sid = "cb-hr-normal"
bus.publish(
Event(event_type=EventType.CB_REQUEST_START, session_id=sid, timestamp=now)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id=sid,
timestamp=now + 0.001,
metadata={"num_tokens": 1024},
)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id=sid,
timestamp=now + 0.010,
metadata={
"num_tokens": 1024,
"fingerprint_hits": 4,
"storage_hits": 2,
"stale_chunks": 0,
"no_gpu_context": False,
"hit_tokens": 512,
"requested_tokens": 1024,
},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.020,
)
)
time.sleep(0.15)
bus.stop()
root = self._root_span(exporter, sid)
assert root is not None
assert root.attributes["hit_tokens"] == 512
assert root.attributes["requested_tokens"] == 1024
assert abs(root.attributes["hit_rate"] - 0.5) < 1e-9
def test_hit_rate_zero_when_requested_tokens_is_zero(self, exporter):
"""CB_LOOKUP_END with requested_tokens=0 yields hit_rate=0.0 without error."""
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
registry = SpanRegistry()
bus.register_subscriber(BlendTracingSubscriber(registry))
bus.start()
now = time.time()
sid = "cb-hr-zero"
bus.publish(
Event(event_type=EventType.CB_REQUEST_START, session_id=sid, timestamp=now)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id=sid,
timestamp=now + 0.001,
metadata={"num_tokens": 0},
)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id=sid,
timestamp=now + 0.010,
metadata={
"num_tokens": 0,
"fingerprint_hits": 0,
"storage_hits": 0,
"stale_chunks": 0,
"no_gpu_context": False,
"hit_tokens": 0,
"requested_tokens": 0,
},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.020,
)
)
time.sleep(0.15)
bus.stop()
root = self._root_span(exporter, sid)
assert root is not None
assert root.attributes["hit_tokens"] == 0
assert root.attributes["requested_tokens"] == 0
assert root.attributes["hit_rate"] == 0.0
def test_total_miss_hit_rate(self, exporter):
"""CB_LOOKUP_END with storage_hits=0 but requested_tokens>0 → hit_rate=0.0."""
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
registry = SpanRegistry()
bus.register_subscriber(BlendTracingSubscriber(registry))
bus.start()
now = time.time()
sid = "cb-hr-miss"
bus.publish(
Event(event_type=EventType.CB_REQUEST_START, session_id=sid, timestamp=now)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id=sid,
timestamp=now + 0.001,
metadata={"num_tokens": 1024},
)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id=sid,
timestamp=now + 0.010,
metadata={
"num_tokens": 1024,
"fingerprint_hits": 0,
"storage_hits": 0,
"stale_chunks": 0,
"no_gpu_context": False,
"hit_tokens": 0,
"requested_tokens": 1024,
},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.020,
)
)
time.sleep(0.15)
bus.stop()
root = self._root_span(exporter, sid)
assert root is not None
assert root.attributes["hit_tokens"] == 0
assert root.attributes["requested_tokens"] == 1024
assert root.attributes["hit_rate"] == 0.0
def test_prefix_hits_attr_set_on_root_span(self, exporter):
"""CB_LOOKUP_END with prefix_hits=2 stamps prefix_hits=2 on root span."""
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
registry = SpanRegistry()
bus.register_subscriber(BlendTracingSubscriber(registry))
bus.start()
now = time.time()
sid = "cb-prefix-hits"
bus.publish(
Event(event_type=EventType.CB_REQUEST_START, session_id=sid, timestamp=now)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id=sid,
timestamp=now + 0.001,
metadata={"num_tokens": 512},
)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id=sid,
timestamp=now + 0.010,
metadata={
"num_tokens": 512,
"fingerprint_hits": 0,
"prefix_hits": 2,
"storage_hits": 2,
"stale_chunks": 0,
"no_gpu_context": False,
"hit_tokens": 512,
"requested_tokens": 512,
},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.020,
)
)
time.sleep(0.15)
bus.stop()
root = self._root_span(exporter, sid)
assert root is not None
assert root.attributes["prefix_hits"] == 2
assert root.attributes["hit_tokens"] == 512
assert root.attributes["hit_rate"] == 1.0
def test_prefix_hits_defaults_to_zero_when_absent(self, exporter):
"""CB_LOOKUP_END without prefix_hits stamps prefix_hits=0 (backward compat)."""
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
registry = SpanRegistry()
bus.register_subscriber(BlendTracingSubscriber(registry))
bus.start()
now = time.time()
sid = "cb-prefix-absent"
bus.publish(
Event(event_type=EventType.CB_REQUEST_START, session_id=sid, timestamp=now)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id=sid,
timestamp=now + 0.001,
metadata={"num_tokens": 256},
)
)
# Omit prefix_hits to simulate an older server payload
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id=sid,
timestamp=now + 0.010,
metadata={
"num_tokens": 256,
"fingerprint_hits": 1,
"storage_hits": 1,
"stale_chunks": 0,
"no_gpu_context": False,
"hit_tokens": 256,
"requested_tokens": 256,
},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.020,
)
)
time.sleep(0.15)
bus.stop()
root = self._root_span(exporter, sid)
assert root is not None
assert root.attributes["prefix_hits"] == 0
def test_hit_rate_includes_prefix_and_non_prefix(self, exporter):
"""V3 hit_rate numerator = prefix_hit_tokens + non_prefix_hit_tokens;
both are also recorded as separate attributes on the root span."""
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
bus.register_subscriber(BlendTracingSubscriber(SpanRegistry()))
bus.start()
now = time.time()
sid = "cb-hr-split"
bus.publish(
Event(event_type=EventType.CB_REQUEST_START, session_id=sid, timestamp=now)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_START,
session_id=sid,
timestamp=now + 0.001,
metadata={"num_tokens": 1024},
)
)
bus.publish(
Event(
event_type=EventType.CB_LOOKUP_END,
session_id=sid,
timestamp=now + 0.010,
metadata={
"prefix_hit_tokens": 256,
"non_prefix_hit_tokens": 256,
"hit_tokens": 512, # = prefix + non_prefix (set by blend_v3)
"requested_tokens": 1024,
},
)
)
bus.publish(
Event(
event_type=EventType.CB_REQUEST_END,
session_id=sid,
timestamp=now + 0.011,
)
)
time.sleep(0.2)
bus.stop()
root = self._root_span(exporter, sid)
assert root is not None
assert root.attributes["prefix_hit_tokens"] == 256
assert root.attributes["non_prefix_hit_tokens"] == 256
assert root.attributes["hit_tokens"] == 512
assert root.attributes["hit_rate"] == 0.5
class TestCBLookupSubspans:
"""V3 lookup sub-spans (cb.fingerprint_match / cb.sparse_prefetch) nest under
cb.lookup, not the cb.request root. The prefix lookup has no cb.* span (it is
traced by mp.lookup_prefetch); prefix_chunks rides on cb.lookup."""
@pytest.fixture
def exporter(self):
exp = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exp))
real_tracer = provider.get_tracer("lmcache_mp.blend")
with (
patch.object(cb_server_module, "_tracer", real_tracer),
patch.object(cb_server_module, "_HAS_OTEL", True),
):
yield exp
exp.shutdown()
def _spans_by_name(self, exporter: InMemorySpanExporter, sid: str):
return {
s.name: s
for s in exporter.get_finished_spans()
if s.attributes.get("session_id") == sid
}
def test_lookup_subspans_nest_under_cb_lookup(self, exporter):
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
bus.register_subscriber(BlendTracingSubscriber(SpanRegistry()))
bus.start()
now = time.time()
sid = "cb-subspans"
seq = [
(EventType.CB_REQUEST_START, {}),
(EventType.CB_LOOKUP_START, {"num_tokens": 1024}),
(EventType.CB_FINGERPRINT_MATCH_START, {}),
(EventType.CB_FINGERPRINT_MATCH_END, {"matches": 7}),
(EventType.CB_SPARSE_PREFETCH_START, {"n_chunks": 5}),
(EventType.CB_SPARSE_PREFETCH_END, {"found_keys": 5}),
(EventType.CB_LOOKUP_END, {"num_tokens": 1024, "prefix_chunks": 2}),
(EventType.CB_REQUEST_END, {}),
]
for i, (et, md) in enumerate(seq):
bus.publish(
Event(
event_type=et,
session_id=sid,
timestamp=now + i * 0.001,
metadata=md,
)
)
time.sleep(0.3)
bus.stop()
spans = self._spans_by_name(exporter, sid)
for name in (
"cb.lookup",
"cb.fingerprint_match",
"cb.sparse_prefetch",
):
assert name in spans, f"missing span {name}; have {sorted(spans)}"
# No cb.prefix_lookup span (traced by mp.lookup_prefetch instead).
assert "cb.prefix_lookup" not in spans
lookup_id = spans["cb.lookup"].context.span_id
for name in ("cb.fingerprint_match", "cb.sparse_prefetch"):
assert spans[name].parent is not None, f"{name} has no parent span"
assert spans[name].parent.span_id == lookup_id, (
f"{name} should nest under cb.lookup"
)
# cb.lookup itself nests under the cb.request root (unchanged behavior).
assert spans["cb.lookup"].parent.span_id == spans["cb.request"].context.span_id
# sub-span metadata propagates as attributes.
assert spans["cb.fingerprint_match"].attributes.get("matches") == "7"
# prefix coverage rides on cb.lookup (no dedicated prefix span).
assert spans["cb.lookup"].attributes.get("prefix_chunks") == "2"
assert spans["cb.sparse_prefetch"].attributes.get("found_keys") == "5"
def test_coordinator_match_span_nests_under_cb_lookup(self, exporter):
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
bus.register_subscriber(BlendTracingSubscriber(SpanRegistry()))
bus.start()
now = time.time()
sid = "cb-coord-match"
seq = [
(EventType.CB_REQUEST_START, {}),
(EventType.CB_LOOKUP_START, {"num_tokens": 1024}),
(EventType.CB_COORDINATOR_MATCH_START, {}),
(EventType.CB_COORDINATOR_MATCH_END, {"matches": 3, "timed_out": False}),
(EventType.CB_LOOKUP_END, {"num_tokens": 1024, "prefix_chunks": 2}),
(EventType.CB_REQUEST_END, {}),
]
for i, (et, md) in enumerate(seq):
bus.publish(
Event(
event_type=et,
session_id=sid,
timestamp=now + i * 0.001,
metadata=md,
)
)
time.sleep(0.3)
bus.stop()
spans = self._spans_by_name(exporter, sid)
assert "cb.coordinator_match" in spans, (
f"missing cb.coordinator_match; have {sorted(spans)}"
)
assert (
spans["cb.coordinator_match"].parent.span_id
== spans["cb.lookup"].context.span_id
), "cb.coordinator_match should nest under cb.lookup"
assert spans["cb.coordinator_match"].attributes.get("matches") == "3"
assert spans["cb.coordinator_match"].attributes.get("timed_out") == "False"
def test_scatter_span_nests_under_cb_retrieve(self, exporter):
bus = EventBus(EventBusConfig(enabled=True, max_queue_size=100))
bus.register_subscriber(BlendTracingSubscriber(SpanRegistry()))
bus.start()
now = time.time()
sid = "cb-scatter"
seq = [
(EventType.CB_REQUEST_START, {}),
(EventType.CB_RETRIEVE_START, {"num_chunks": 5}),
(
EventType.CB_SCATTER_START,
{
"scattered_tokens": 1280,
"n_prefix": 1,
"n_shifted": 4,
"dropped": 0,
},
),
(EventType.CB_SCATTER_END, {}),
(EventType.CB_RETRIEVE_END, {}),
(EventType.CB_REQUEST_END, {}),
]
for i, (et, md) in enumerate(seq):
bus.publish(
Event(
event_type=et,
session_id=sid,
timestamp=now + i * 0.001,
metadata=md,
)
)
time.sleep(0.3)
bus.stop()
spans = self._spans_by_name(exporter, sid)
assert "cb.scatter" in spans, f"missing cb.scatter; have {sorted(spans)}"
assert "cb.retrieve" in spans
assert (
spans["cb.scatter"].parent.span_id == spans["cb.retrieve"].context.span_id
), "cb.scatter should nest under cb.retrieve"
assert spans["cb.scatter"].attributes.get("scattered_tokens") == "1280"
assert spans["cb.scatter"].attributes.get("n_shifted") == "4"
assert spans["cb.scatter"].attributes.get("dropped") == "0"