Repository navigation
feat: add capability discovery to the observability contract - #474
Conversation
Extends the get_capabilities() record from ovg-project#385 with backend and per-integration capabilities, additive feature keys for landed and planned surfaces, and counter enumeration. No schema bump: new optional keys are additive and consumers treat a missing key as unsupported. Read-only, no behavior change. Implements item (3) of ovg-project#375.
Part of ovg-project#375, item (5) phase 1; stacked on ovg-project#474. KVCacheManager._post_init() runs in a daemon thread. When it raised (KV tensors never created, worker IPC unreachable as in ovg-project#471) the error died with the thread while _post_init_done still opened the gate, so callers walked into a pool with no null block and no prealloc thread, and nothing queryable said why. Each pool now carries a thread-safe lifecycle phase (kvcached.lifecycle: INITIALIZING, READY, DEGRADED, FAILED) set by _post_init(), clear() and the map/unmap broadcast callbacks, plus wait_ready(timeout), which re-raises the captured init error, and lifecycle_phase/lifecycle_error for polling. KVCachePoolSnapshot gains an additive lifecycle_phase field. Broadcast failures follow the rule agreed in ovg-project#375: the outcome across ranks is unknown, a timeout included, so the pool degrades. Per-rank delivery classification (the provably-safe carve-out) is deferred until ovg-project#373's rewrite of the broadcast loops lands. get_capabilities()["features"]["lifecycle_readiness"] advertises the surface under the record from ovg-project#474. Record-only: the existing entry points and _post_init_done behave exactly as before.
rishabhsinha17
left a comment
There was a problem hiding this comment.
Reviewing as the item-5 consumer: the lifecycle/readiness key from #375 hangs off this record, and the stacked PR for it is #478, so these are the things I hit while consuming the shape. Nothing here blocks; 1 and 2 are the ones worth a reply, the rest are optional.
-
Record shape works for the lifecycle key.
features(kvcached/observability.py:197-212) is a flat map of bools with a comment per key and an explicit "unlanded surfaces report False" rule (:189-192).lifecycle_readiness: Trueslots in afterruntime_reservation_reporting(:211) with the same shape, no parallel key, no schema bump. Later the phase vocabulary should be enumerable the wayoperation_countersis (:218), a top-levellifecycle_phaseslist; not for this PR. -
"Queryable without importing torch" (docstring :147-149 and the PR body) is not true at package level.
kvcached/__init__.py:14-22imports torch unconditionally and raises ImportError without it, soimport kvcached.observabilityneeds torch installed or stubbed; the tests only run torch-free because tests/test_observability.py:12-13 puts a stubtorchintosys.modules. Suggest "without importing an engine shim or attaching to a running engine", which is what the static record actually buys. -
Static
integrationsclaims, checked against the shims. All accurate today:- vllm
attention_types(:154) match the guard at kvcached/integration/vllm/interfaces.py:316; sglang (:166) match kvcached/integration/sglang/interfaces.py:123. kv_layouts: ["NHD"]: vllm rejects anything else (vllm/interfaces.py:319). sglang skips the layout check for MLA (sglang/interfaces.py:127,if not is_mla and kv_layout != "NHD"), so for MLA the argument is ignored rather than validated. One word in the comment ("validated") would cover it.page_aware_eviction: vllm True via_page_aligned_victimsand thepage_awareflag (vllm/patches.py:699-771); sglang False is right, the RadixCache patch only caps evictable size (sglang/patches.py:1477-1541).worker_ipc: Truefor both: vllm/interfaces.py:67,93 and sglang/interfaces.py:61 start the listener.- sglang
hybrid_linear_state_pooling: Trueviaalloc_mamba_states(sglang/interfaces.py:217).
Drift risk: nothing ties :154/:166 to those guards. Cheapest fix is to hoist each guard's literal into a module constant (
SUPPORTED_ATTENTION_TYPES) and add one test in the existing mocked-shim pattern (test_observability.py:325 already imports the vllm shim under stubs) asserting the record equals the constant; same forkv_layouts. Also,hybrid_linear_state_poolingmeans two different things (:156-158 same pool via HYBRID_LINEAR; :168-170 a separate entry point), so a consumer cannot tell from the bool whether mamba state appears in the pool snapshot; a note in the comment is enough for now. - vllm
-
backendsvalues are import-time env reads.page_size_bytesandcontiguous_layout_default(:137-138) come fromkvcached.utils(utils.py:133, :177), which readKVCACHED_PAGE_SIZE_MBand the layout env at import. Correct in the engine process; a control plane importing kvcached in its own process gets its own env, not the engine's.KVCachePoolSnapshot.page_size_bytes(:92, :309) is the per-pool truth, so the docstring could point live consumers there. -
No private-field leakage in the record.
test_capabilities_need_no_private_field_access(:477-496) checks key names only, which is fine for a static record. -
operation_counterslives twice:features.operation_counters: False(:209) and the top-level list (:218). Two places to flip when #410 lands; a one-line test that the list is non-empty iff the flag is True would keep them coupled. Optional. -
Small test gaps:
backends["page_size_bytes"]is only checked> 0(:435-436), could assert equality withkvcached.utils.PAGE_SIZE; no test thatintegrations[*]["attention_types"]match the shim guards (see 3); no coupling test for 6.
shipiyouniao
left a comment
There was a problem hiding this comment.
Thanks for picking up item (3). I checked the implementation against the current vLLM/SGLang shims and ran tests/test_observability.py locally (14 passed). The additive get_capabilities() shape, missing-key compatibility rule, and JSON contract all look good.
Before this becomes a stable surface, I think two semantics should be tightened:
backends.page_size_bytesis the import-time value from this process, not an allocator capability and not necessarily the live engine's value when queried out of process. Please either name itdefault_page_size_bytesand document that scope, or leave runtime page size toKVCachePoolSnapshot.page_size_bytes.contiguous_layout_defaultalready makes the distinction explicit.hybrid_linear_state_pooling: truecurrently represents two operationally different contracts: vLLM places the state in the HYBRID_LINEAR pool, while SGLang uses a separate allocation entry point. A consumer cannot distinguish those shapes from the boolean. An additive machine-readable mode such ashybrid_linear_state_pooling_modewould keep feature detection useful without requiring consumers to parse comments.
I agree with the existing review that the package-level "without importing torch" wording should also be narrowed. I did not find another behavioral blocker.
|
Thank you both, this was really helpful! I addressed everything in Page size — renamed to Hybrid linear mode — added Torch wording — agreed on this. The tests were only passing torch-free because of the stub at Drift risk — both shims now define Also added the counter coupling test (and renamed the list to I left the top-level The record shape is unchanged by all of this, so
|
Part of ovg-project#375, item (5) phase 1; stacked on ovg-project#474. KVCacheManager._post_init() runs in a daemon thread. When it raised (KV tensors never created, worker IPC unreachable as in ovg-project#471) the error died with the thread while _post_init_done still opened the gate, so callers walked into a pool with no null block and no prealloc thread, and nothing queryable said why. Each pool now carries a thread-safe lifecycle phase (kvcached.lifecycle: INITIALIZING, READY, DEGRADED, FAILED) set by _post_init(), clear() and the unmap broadcast callback, plus wait_ready(timeout), which re-raises the captured init error, and lifecycle_phase/lifecycle_error for polling. KVCachePoolSnapshot gains an additive lifecycle_phase field. Unmap broadcast failures follow the rule agreed in ovg-project#375: the outcome across ranks is unknown, a timeout included, and no caller rolls the ledger back, so the pool degrades. Map broadcast failures deliberately do not transition lifecycle state in phase 1: they are routinely the expected co-tenancy capacity miss that _alloc() rolls back and reports as a scheduling miss (ovg-project#453), and the wrapper cannot tell that recoverable miss from an unknown outcome because tp_ipc_util raises one RuntimeError shape for every per-rank failure. Per-rank classification (safe miss / unknown / confirmed partial) is deferred until ovg-project#373's rewrite of the broadcast loops lands. get_capabilities()["features"]["lifecycle_readiness"] advertises the surface under the record from ovg-project#474. Record-only: the existing entry points and _post_init_done behave exactly as before.
Part of ovg-project#375, item (5) phase 1; stacked on ovg-project#474. KVCacheManager._post_init() runs in a daemon thread. When it raised (KV tensors never created, worker IPC unreachable as in ovg-project#471) the error died with the thread while _post_init_done still opened the gate, so callers walked into a pool with no null block and no prealloc thread, and nothing queryable said why. Each pool now carries a thread-safe lifecycle phase (kvcached.lifecycle: INITIALIZING, READY, DEGRADED, FAILED) set by _post_init(), clear() and the unmap broadcast callback, plus wait_ready(timeout), which re-raises the captured init error, and lifecycle_phase/lifecycle_error for polling. KVCachePoolSnapshot gains an additive lifecycle_phase field. Unmap broadcast failures follow the rule agreed in ovg-project#375: the outcome across ranks is unknown, a timeout included, and no caller rolls the ledger back, so the pool degrades. Map broadcast failures deliberately do not transition lifecycle state in phase 1: they are routinely the expected co-tenancy capacity miss that _alloc() rolls back and reports as a scheduling miss (ovg-project#453), and the wrapper cannot tell that recoverable miss from an unknown outcome because tp_ipc_util raises one RuntimeError shape for every per-rank failure. Per-rank classification (safe miss / unknown / confirmed partial) is deferred until ovg-project#373's rewrite of the broadcast loops lands. clear() re-enters INITIALIZING for the teardown window on READY and DEGRADED pools alike, so wait_ready() holds while the prealloc thread is stopped and mappings are released; a DEGRADED pool settles back to DEGRADED with its first cause preserved rather than READY. Stickiness preserves the cause across reinitialization, not the phase during it. FAILED stays put. get_capabilities()["features"]["lifecycle_readiness"] advertises the surface under the record from ovg-project#474. Record-only: the existing entry points and _post_init_done behave exactly as before.
rishabhsinha17
left a comment
There was a problem hiding this comment.
Re-checked f651fdf and f2da050 against my 09-02 review, point by point.
- Page size. Renamed to
default_page_size_byteswith the process-scope comment and the pointer toKVCachePoolSnapshot.page_size_bytes(observability.py:137-142), and the test now asserts equality withkvcached.utils.PAGE_SIZEplus absence of the old key (test_observability.py:439-440). - Hybrid mode.
hybrid_linear_state_pooling_modeis additive next to the bool,unified_poolvsseparate_allocation(observability.py:167, :184), the comments now say whether that state appears in the pool snapshot, and test_observability.py:456 pins both values. - Torch wording. Docstring now reads "without importing an engine shim or attaching to a running engine" with the
kvcached/__init__.pycaveat (observability.py:151-156). One leftover, the PR body still has the old "queryable without importing torch" line plus the pre-renameoperation_countersbullet and the 14-test count; worth a quick edit since the body tends to become the squash message. - Drift coupling. Both shims hoist the guards into
SUPPORTED_ATTENTION_TYPES/SUPPORTED_KV_LAYOUTS(vllm/interfaces.py:40-41, used at :322 and :325; sglang/interfaces.py:40-41, used at :130 and :134), the new guard conditions are value-identical to the old literals, and test_observability.py:573 imports each shim under stubs and asserts the record equals the constants for both attention types and kv layouts. The MLA layout caveat is recorded on both sides (observability.py:176-178, sglang/interfaces.py:38-39). - Counters.
operation_counter_namesrename plus the bool(names) == flag coupling test (observability.py:234, test_observability.py:476).
Ran tests/test_observability.py on f2da050 locally, 17 passed, and CI is green on 3.9 through 3.13. lifecycle_phases deferred to item (5) as agreed, and #478 still slots lifecycle_readiness in after runtime_reservation_reporting with no schema bump. Approving; the body wording is not blocking.
Part of ovg-project#375, item (5) phase 1; stacked on ovg-project#474. KVCacheManager._post_init() runs in a daemon thread. When it raised (KV tensors never created, worker IPC unreachable as in ovg-project#471) the error died with the thread while _post_init_done still opened the gate, so callers walked into a pool with no null block and no prealloc thread, and nothing queryable said why. Each pool now carries a thread-safe lifecycle phase (kvcached.lifecycle: INITIALIZING, READY, DEGRADED, FAILED) set by _post_init(), clear() and the unmap broadcast callback, plus wait_ready(timeout), which re-raises the captured init error, and lifecycle_phase/lifecycle_error for polling. KVCachePoolSnapshot gains an additive lifecycle_phase field. Unmap broadcast failures follow the rule agreed in ovg-project#375: the outcome across ranks is unknown, a timeout included, and no caller rolls the ledger back, so the pool degrades. Map broadcast failures deliberately do not transition lifecycle state in phase 1: they are routinely the expected co-tenancy capacity miss that _alloc() rolls back and reports as a scheduling miss (ovg-project#453), and the wrapper cannot tell that recoverable miss from an unknown outcome because tp_ipc_util raises one RuntimeError shape for every per-rank failure. Per-rank classification (safe miss / unknown / confirmed partial) is deferred until ovg-project#373's rewrite of the broadcast loops lands. clear() re-enters INITIALIZING for the teardown window on READY and DEGRADED pools alike, so wait_ready() holds while the prealloc thread is stopped and mappings are released; a DEGRADED pool settles back to DEGRADED with its first cause preserved rather than READY. Stickiness preserves the cause across reinitialization, not the phase during it. FAILED stays put. get_capabilities()["features"]["lifecycle_readiness"] advertises the surface under the record from ovg-project#474. Record-only: the existing entry points and _post_init_done behave exactly as before.
shipiyouniao
left a comment
There was a problem hiding this comment.
The capability semantics from my previous review are fixed: process-local defaults are named explicitly, hybrid pooling modes are distinguishable, and the record is tied to the shim guards. I re-ran the current head: 17 passed.
There is one integration-test blocker after today's #413 merge. A clean merge of this head with current main (cf46ed7) gives 16 passed / 1 failed in tests/test_observability.py. The new shim-loading fixture needs the same device-resolver stub already included in #478's rebased dependency commits. No allocator behavior change is needed. The PR body also still contains the older torch-free wording and counter-list name; those are documentation nits, not the blocker.
The blocking device-resolver fixture issue is fixed in edbb292. Validation against current main and CI passed. Dismissing my outdated change request; the author can review the appended fix.
Part of ovg-project#375, item (5) phase 1; stacked on ovg-project#474. KVCacheManager._post_init() runs in a daemon thread. When it raised (KV tensors never created, worker IPC unreachable as in ovg-project#471) the error died with the thread while _post_init_done still opened the gate, so callers walked into a pool with no null block and no prealloc thread, and nothing queryable said why. Each pool now carries a thread-safe lifecycle phase (kvcached.lifecycle: INITIALIZING, READY, DEGRADED, FAILED) set by _post_init(), clear() and the unmap broadcast callback, plus wait_ready(timeout), which re-raises the captured init error, and lifecycle_phase/lifecycle_error for polling. KVCachePoolSnapshot gains an additive lifecycle_phase field. Unmap broadcast failures follow the rule agreed in ovg-project#375: the outcome across ranks is unknown, a timeout included, and no caller rolls the ledger back, so the pool degrades. Map broadcast failures deliberately do not transition lifecycle state in phase 1: they are routinely the expected co-tenancy capacity miss that _alloc() rolls back and reports as a scheduling miss (ovg-project#453), and the wrapper cannot tell that recoverable miss from an unknown outcome because tp_ipc_util raises one RuntimeError shape for every per-rank failure. Per-rank classification (safe miss / unknown / confirmed partial) is deferred until ovg-project#373's rewrite of the broadcast loops lands. clear() re-enters INITIALIZING for the teardown window on READY and DEGRADED pools alike, so wait_ready() holds while the prealloc thread is stopped and mappings are released; a DEGRADED pool settles back to DEGRADED with its first cause preserved rather than READY. Stickiness preserves the cause across reinitialization, not the phase during it. FAILED stays put. get_capabilities()["features"]["lifecycle_readiness"] advertises the surface under the record from ovg-project#474. Record-only: the existing entry points and _post_init_done behave exactly as before.
The ovg-project#474 capability test's tp_ipc_util stub predates this branch's shutdown_kvcached import of stop_worker_listener_threads, so the vllm interfaces import failed under it after the rebase. Mirror the other two stub sites in this file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ovg-project#474 capability test's tp_ipc_util stub predates this branch's shutdown_kvcached import of stop_worker_listener_threads, so the vllm interfaces import failed under it after the rebase. Mirror the other two stub sites in this file.
Implements item (3), capability discovery, from #375.
Extends the existing
get_capabilities()contract from #385 rather than adding aparallel entry point or a second schema, per the coordination in that thread.
What it adds
features— three additive keys.instance_memory_limitreportsTrue(landed in feat: add revisioned instance memory limits #414);
operation_countersandruntime_reservation_reportingreport
Falseuntil Add KV cache operation observability #410 and item (4) land. Unlanded surfaces are reportedFalserather than omitted, so a consumer writes the detection once and thesame code starts returning
Truewhen the surface ships.backends— allocator-level capabilities shared by every integration:KV pooling, elastic capacity, resize, trim, page size, the contiguous-layout
default, and
non_kv_memory_management: Falseto record the boundary agreedupthread (kvcached accounts for non-KV memory, never manages it).
integrations— per-engine entries for vLLM and SGLang covering attentiontypes, KV layouts, hybrid/linear state pooling, prefix caching, page-aware
eviction, and worker IPC. These are real code-level distinctions today: only
the vLLM shim accepts
HYBRID_LINEARthroughalloc_kv_cache(), and only vLLMevicts prefix-cache blocks page-aware. Reported statically from the shim
contracts, so the record is queryable without importing torch or attaching to a
running engine.
operation_counters— empty list, enumerating the counters exposed onceAdd KV cache operation observability #410 lands.
Lifecycle/readiness is deliberately not included here: per the split agreed with
@rishabhsinha17, that key belongs to item (5) and lands with it.
Compatibility
No schema bump. New optional keys are additive, so
schema_versionstayskvcached.observability.v1; consumers must treat a missing key as unsupported.The rule is documented on
get_capabilities(). No behavior changes and no newstate — the record is read-only and derived from existing contracts.
Tests
Five CPU-only contract tests added to
tests/test_observability.py, following theexisting mocked-extension pattern:
Falserather than being omitted;HYBRID_LINEARsplit;tests/test_observability.pypasses (14 tests), as dotest_page_allocator_snapshot.pyand
test_memory_limit_control.py. ruff, isort, codespell, and mypy pass viapre-commit.